diff --git a/PLANNED & NOT PLANNED.md b/PLANNED & NOT PLANNED.md index f24ff71..830de62 100644 --- a/PLANNED & NOT PLANNED.md +++ b/PLANNED & NOT PLANNED.md @@ -26,8 +26,8 @@ - Permission management - Full fledge package manager / app store - - Viewing app icons and screenshots from remotes - - Reading and leaving app reviews - - Auto updates + - Viewing app icons and screenshots from remotes + - Reading and leaving app reviews + - Auto updates - Management of any non Flatpak packages - Supporting any repackages (apart from nixpkgs should that arise) diff --git a/data/io.github.flattool.Warehouse.metainfo.xml.in b/data/io.github.flattool.Warehouse.metainfo.xml.in index 4fda617..8d7c6c3 100644 --- a/data/io.github.flattool.Warehouse.metainfo.xml.in +++ b/data/io.github.flattool.Warehouse.metainfo.xml.in @@ -8,31 +8,31 @@ GPL-3.0-only Manage all things Flatpak -

Warehouse provides a simple UI to control complex Flatpak options, all without resorting to the command line.

-

Features:

- +

Warehouse provides a simple UI to control complex Flatpak options, all without resorting to the command line.

+

Features:

+
- #AECEF4 - #072F5E + #AECEF4 + #072F5E - keyboard - pointing - touch + keyboard + pointing + touch - 330 + 330 https://github.com/flattool/warehouse @@ -41,268 +41,268 @@ https://weblate.fyralabs.com/projects/flattool/warehouse/ https://ko-fi.com/heliguy - - https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/packages_page_wide.png - Manage Installed Packages in Three Pane UI - - - https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/propteries_page_skinny.png - Properties Page in Narrow Window - - - https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/remotes_page_wide.png - Manage Installed Remotes and Add New Remotes - - - https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/data_page_wide.png - Manage Apps' User Data - - - https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/snapshots_page_wide.png - Backup Apps' User Data - - - https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_wide.png - Install New Packages from Files or Remotes - - - https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_skinny.png - Install Page in Narrow Window - + + https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/packages_page_wide.png + Manage Installed Packages in Three Pane UI + + + https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/propteries_page_skinny.png + Properties Page in Narrow Window + + + https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/remotes_page_wide.png + Manage Installed Remotes and Add New Remotes + + + https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/data_page_wide.png + Manage Apps' User Data + + + https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/snapshots_page_wide.png + Backup Apps' User Data + + + https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_wide.png + Install New Packages from Files or Remotes + + + https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_skinny.png + Install Page in Narrow Window + - - -

New Features

-
    -
  • All new UI to make using Warehouse's features easier
  • -
  • UI now better adapts to larger window sizes
  • -
  • UI now better adapts to smaller window sizes
  • -
  • Improved UI for installing packages from remotes
  • -
  • Snapshots can be given names
  • -
  • Packages can be reinstalled with a few clicks
  • -
  • User Data can now be sorted in multiple ways
  • -
  • Active User Data can be browsed just as easily as leftover User Data
  • -
  • Custom installation locations are now supported
  • -
  • Leftover Snapshots are now shown
  • -
  • Apps can be reinstalled from leftover Snapshots
  • -
  • Installation location of disabled remotes is now shown
  • -
-

Changes

-
    -
  • Packages list filter options are now easier to understand, and more predictable with how they are applied
  • -
  • Improved keyboard shortcuts for quick navigation
  • -
  • The Downgrades (renamed to Change Version) interface now shows the currently installed version
  • -
  • Long running processes now have progress bars and can be canceled
  • -
  • Better status icons for End of Life, Masked, and Pinned packages
  • -
  • Warehouse no longer disables closing its window when long running processes are happening
  • -
  • Refreshing now shows a loading animation
  • -
-

Bug Fixes and Performance Improvements

-
    -
  • Warehouse is now faster to open
  • -
  • Getting system information is now faster
  • -
  • Long running processes no longer freeze the app
  • -
  • Refreshing is no longer possible when long running processes are happening
  • -
-
-
- - -

Bug Fixes

-
    -
  • Downgrade Window no longer silently fails when downgrading a masked Flatpak, and instead, downgrades it
  • -
  • When downgrading and masking system Flatpaks, the password prompt only happens once instead of twice
  • -
-
-
- - -

Bug Fixes

-
    -
  • Install from the Web no longer has issues on systems with only one remote
  • -
-
-
- - -

Bug Fixes

-
    -
  • App Properties no longer has issues with information containing colon characters
  • -
-

Previous Releases's New Features and Changes

-
    -
  • The main list has a status page while refreshing
  • -
  • Add ability to pin and unpin runtimes
  • -
  • App Properties shows license and proper commit information
  • -
  • App Properties better shows the app's name and description
  • -
  • Update to GNOME 46 GTK Technologies
  • -
  • Updated translations
  • -
-

Previous Releases's Bug Fixes

-
    -
  • When an attempt to run an app fails, correct runtime error information is shown
  • -
  • Install From The Web no longer behaves incorrectly on remote installations with options
  • -
  • The rare chance that Install From The Web selects a disabled remote has been fixed
  • -
-
-
- - -

New Features and Changes

-
    -
  • The main list has a status page while refreshing
  • -
  • Add ability to pin and unpin runtimes
  • -
  • App Properties shows license and proper commit information
  • -
  • App Properties better shows the app's name and description
  • -
  • Update to GNOME 46 GTK Technologies
  • -
  • Updated translations
  • -
-

Bug Fixes

-
    -
  • When an attempt to run an app fails, correct runtime error information is shown
  • -
  • Install From The Web no longer behaves incorrectly on remote installations with options
  • -
  • The rare chance that Install From The Web selects a disabled remote has been fixed
  • -
-
-
- - -

Bug Fixes

-
    -
  • Main list is no longer scrolled to the bottom on launch
  • -
  • Leftover Data window no longer tries to use a different window for toast messages
  • -
  • Fix issue causing Downgrade window to not be able to downgrade user installation Flatpaks
  • -
  • Fix the accidental removal of translations
  • -
  • Fix issue causing Reset Filters button to be clickable upon Filter Window opening even when the filters are default
  • -
-
-
- - -

New Features and Changes

-
    -
  • Flatpaks can now be installed from the web, from any added remote
  • -
  • Filters are now saved and restored between sessions
  • -
  • Filters now apply live
  • -
  • Move Refresh Button into Main Menu
  • -
  • Period, 0 to 9, and underscores are now allowed in new Custom Remote names
  • -
  • Updated translations
  • -
-

Bug Fixes

-
    -
  • Hide Show Disabled Remotes button when there aren't any
  • -
  • Fix Batch Snapshots accidentally triggering Select All
  • -
  • Misc UI element tooltips and alignments
  • -
-
-
- - -

New Features and Changes

-
    -
  • View disabled remotes, enable and disable remotes, and set a filter for a remote in the Manage Remotes window
  • -
  • Added a new Snapshot feature. Snapshots can be created and applied at any time for quick saving app user data
  • -
  • Added a batch action to create Snapshots
  • -
  • Revamped the Properties window, and it can also open app details in the software store
  • -
  • Merged the Popular Remotes window into the Manage Remotes window
  • -
  • Removed labels in the main list, placing them in popup text buttons instead, to save room
  • -
  • Added progress bars to show progress of batch actions
  • -
  • Empty search pages now display a message
  • -
  • Added a troubleshooting information page
  • -
  • Added a donation link
  • -
  • Added translations (NL, FR, RU, SV, UK)
  • -
-

Bug Fixes

-
    -
  • Launching Warehouse no longer hangs when grabbing the list of Flatpaks
  • -
  • Unexpected errors are caught more often and handled better
  • -
  • Fixed a few typos
  • -
-
-
- - -

New Features and Changes

-
    -
  • Names, IDs, Refs, and Launch Commands can now be copied from a dropdown
  • -
  • Updates can now be disabled and enabled for Flatpaks
  • -
  • Warehouse can now downgrade Flatpaks
  • -
  • Apps can be now be ran from Warehouse
  • -
  • Control + Keypad Enter now also toggles select mode
  • -
  • Added translations (HG, ES, TH)
  • -
-

Bug Fixes

-
    -
  • The runtime filter button in the properties window now only shows when that runtime is a dependent runtime
  • -
  • Removed the Select All keyboard shortcut as it interfered with the search bar
  • -
-
-
- - -
    -
  • The main list of Flatpaks is now sorted by name instead of ID
  • -
  • Warehouse can now be found in your app menu by searching for "flatpak"
  • -
  • Corrected typo in the previous release's change log
  • -
-
-
- - -

New Features and Changes

-
    -
  • Flatpaks files can be installed with a drag and drop or from a file selection
  • -
  • Flatpak remotes can be added with a drag and drop or from a file selection
  • -
  • Apps can now be filtered by dependent runtimes
  • -
  • Properties of a runtime now shows a button to show only apps that rely on the runtime
  • -
  • A loading indicator is now shown when adding a remote
  • -
  • Added Webkit Testing to the list of popular remotes
  • -
  • Added a search bar to the Leftover Data window
  • -
  • Added a button to open the entire user data folder the Leftover Data window
  • -
  • Added a button on each row in the Leftover Data list to open them directly
  • -
  • Changed margins of lists to improve legibility
  • -
  • F10 now opens the main menu
  • -
-

Bug Fixes and Stability Improvements

-
    -
  • Fixed a crash that would sometimes occur when opening the Leftover Data window
  • -
  • Popular remotes are now named correcting and have proper descriptions
  • -
  • The filter button now disables when the Filter window is closed by the keyboard
  • -
  • The default filter is no longer allowed to be set as a new filter
  • -
-
-
- - -

Emergency Bug Fix

-
    -
  • Fix error causing a crash on Linux Mint
  • -
  • Correct typo in the app summary
  • -
-
-
- + + +

New Features

+
    +
  • All new UI to make using Warehouse's features easier
  • +
  • UI now better adapts to larger window sizes
  • +
  • UI now better adapts to smaller window sizes
  • +
  • Improved UI for installing packages from remotes
  • +
  • Snapshots can be given names
  • +
  • Packages can be reinstalled with a few clicks
  • +
  • User Data can now be sorted in multiple ways
  • +
  • Active User Data can be browsed just as easily as leftover User Data
  • +
  • Custom installation locations are now supported
  • +
  • Leftover Snapshots are now shown
  • +
  • Apps can be reinstalled from leftover Snapshots
  • +
  • Installation location of disabled remotes is now shown
  • +
+

Changes

+
    +
  • Packages list filter options are now easier to understand, and more predictable with how they are applied
  • +
  • Improved keyboard shortcuts for quick navigation
  • +
  • The Downgrades (renamed to Change Version) interface now shows the currently installed version
  • +
  • Long running processes now have progress bars and can be canceled
  • +
  • Better status icons for End of Life, Masked, and Pinned packages
  • +
  • Warehouse no longer disables closing its window when long running processes are happening
  • +
  • Refreshing now shows a loading animation
  • +
+

Bug Fixes and Performance Improvements

+
    +
  • Warehouse is now faster to open
  • +
  • Getting system information is now faster
  • +
  • Long running processes no longer freeze the app
  • +
  • Refreshing is no longer possible when long running processes are happening
  • +
+
+
+ + +

Bug Fixes

+
    +
  • Downgrade Window no longer silently fails when downgrading a masked Flatpak, and instead, downgrades it
  • +
  • When downgrading and masking system Flatpaks, the password prompt only happens once instead of twice
  • +
+
+
+ + +

Bug Fixes

+
    +
  • Install from the Web no longer has issues on systems with only one remote
  • +
+
+
+ + +

Bug Fixes

+
    +
  • App Properties no longer has issues with information containing colon characters
  • +
+

Previous Releases's New Features and Changes

+
    +
  • The main list has a status page while refreshing
  • +
  • Add ability to pin and unpin runtimes
  • +
  • App Properties shows license and proper commit information
  • +
  • App Properties better shows the app's name and description
  • +
  • Update to GNOME 46 GTK Technologies
  • +
  • Updated translations
  • +
+

Previous Releases's Bug Fixes

+
    +
  • When an attempt to run an app fails, correct runtime error information is shown
  • +
  • Install From The Web no longer behaves incorrectly on remote installations with options
  • +
  • The rare chance that Install From The Web selects a disabled remote has been fixed
  • +
+
+
+ + +

New Features and Changes

+
    +
  • The main list has a status page while refreshing
  • +
  • Add ability to pin and unpin runtimes
  • +
  • App Properties shows license and proper commit information
  • +
  • App Properties better shows the app's name and description
  • +
  • Update to GNOME 46 GTK Technologies
  • +
  • Updated translations
  • +
+

Bug Fixes

+
    +
  • When an attempt to run an app fails, correct runtime error information is shown
  • +
  • Install From The Web no longer behaves incorrectly on remote installations with options
  • +
  • The rare chance that Install From The Web selects a disabled remote has been fixed
  • +
+
+
+ + +

Bug Fixes

+
    +
  • Main list is no longer scrolled to the bottom on launch
  • +
  • Leftover Data window no longer tries to use a different window for toast messages
  • +
  • Fix issue causing Downgrade window to not be able to downgrade user installation Flatpaks
  • +
  • Fix the accidental removal of translations
  • +
  • Fix issue causing Reset Filters button to be clickable upon Filter Window opening even when the filters are default
  • +
+
+
+ + +

New Features and Changes

+
    +
  • Flatpaks can now be installed from the web, from any added remote
  • +
  • Filters are now saved and restored between sessions
  • +
  • Filters now apply live
  • +
  • Move Refresh Button into Main Menu
  • +
  • Period, 0 to 9, and underscores are now allowed in new Custom Remote names
  • +
  • Updated translations
  • +
+

Bug Fixes

+
    +
  • Hide Show Disabled Remotes button when there aren't any
  • +
  • Fix Batch Snapshots accidentally triggering Select All
  • +
  • Misc UI element tooltips and alignments
  • +
+
+
+ + +

New Features and Changes

+
    +
  • View disabled remotes, enable and disable remotes, and set a filter for a remote in the Manage Remotes window
  • +
  • Added a new Snapshot feature. Snapshots can be created and applied at any time for quick saving app user data
  • +
  • Added a batch action to create Snapshots
  • +
  • Revamped the Properties window, and it can also open app details in the software store
  • +
  • Merged the Popular Remotes window into the Manage Remotes window
  • +
  • Removed labels in the main list, placing them in popup text buttons instead, to save room
  • +
  • Added progress bars to show progress of batch actions
  • +
  • Empty search pages now display a message
  • +
  • Added a troubleshooting information page
  • +
  • Added a donation link
  • +
  • Added translations (NL, FR, RU, SV, UK)
  • +
+

Bug Fixes

+
    +
  • Launching Warehouse no longer hangs when grabbing the list of Flatpaks
  • +
  • Unexpected errors are caught more often and handled better
  • +
  • Fixed a few typos
  • +
+
+
+ + +

New Features and Changes

+
    +
  • Names, IDs, Refs, and Launch Commands can now be copied from a dropdown
  • +
  • Updates can now be disabled and enabled for Flatpaks
  • +
  • Warehouse can now downgrade Flatpaks
  • +
  • Apps can be now be ran from Warehouse
  • +
  • Control + Keypad Enter now also toggles select mode
  • +
  • Added translations (HG, ES, TH)
  • +
+

Bug Fixes

+
    +
  • The runtime filter button in the properties window now only shows when that runtime is a dependent runtime
  • +
  • Removed the Select All keyboard shortcut as it interfered with the search bar
  • +
+
+
+ + +
    +
  • The main list of Flatpaks is now sorted by name instead of ID
  • +
  • Warehouse can now be found in your app menu by searching for "flatpak"
  • +
  • Corrected typo in the previous release's change log
  • +
+
+
+ + +

New Features and Changes

+
    +
  • Flatpaks files can be installed with a drag and drop or from a file selection
  • +
  • Flatpak remotes can be added with a drag and drop or from a file selection
  • +
  • Apps can now be filtered by dependent runtimes
  • +
  • Properties of a runtime now shows a button to show only apps that rely on the runtime
  • +
  • A loading indicator is now shown when adding a remote
  • +
  • Added Webkit Testing to the list of popular remotes
  • +
  • Added a search bar to the Leftover Data window
  • +
  • Added a button to open the entire user data folder the Leftover Data window
  • +
  • Added a button on each row in the Leftover Data list to open them directly
  • +
  • Changed margins of lists to improve legibility
  • +
  • F10 now opens the main menu
  • +
+

Bug Fixes and Stability Improvements

+
    +
  • Fixed a crash that would sometimes occur when opening the Leftover Data window
  • +
  • Popular remotes are now named correcting and have proper descriptions
  • +
  • The filter button now disables when the Filter window is closed by the keyboard
  • +
  • The default filter is no longer allowed to be set as a new filter
  • +
+
+
+ + +

Emergency Bug Fix

+
    +
  • Fix error causing a crash on Linux Mint
  • +
  • Correct typo in the app summary
  • +
+
+
+

New Features and Changes

-
    -
  • Choose from a list of popular remotes when adding a new remote
  • -
  • App properties now shows the runtime that the app relies on
  • -
  • Apps and runtimes that are End of Life are now noted as such
  • -
  • Window size and state is remembered between sessions
  • -
-

Bug Fixes and Performance Improvements

-
    -
  • The UI no longer freezes when uninstalling apps
  • -
  • The UI no longer freezes when getting file sizes of large files
  • -
  • Toggling batch mode no longer causes a freeze
  • -
  • Selecting all apps no longer causes a freeze
  • -
  • Applying and removing a filter no longer causes a freeze
  • -
  • Fixed issue where the no remotes status page would not be removed when a new remote was added
  • -
+
    +
  • Choose from a list of popular remotes when adding a new remote
  • +
  • App properties now shows the runtime that the app relies on
  • +
  • Apps and runtimes that are End of Life are now noted as such
  • +
  • Window size and state is remembered between sessions
  • +
+

Bug Fixes and Performance Improvements

+
    +
  • The UI no longer freezes when uninstalling apps
  • +
  • The UI no longer freezes when getting file sizes of large files
  • +
  • Toggling batch mode no longer causes a freeze
  • +
  • Selecting all apps no longer causes a freeze
  • +
  • Applying and removing a filter no longer causes a freeze
  • +
  • Fixed issue where the no remotes status page would not be removed when a new remote was added
  • +
- +

First release of Warehouse

diff --git a/io.github.flattool.Warehouse.json b/io.github.flattool.Warehouse.json index caeee60..9c77da2 100644 --- a/io.github.flattool.Warehouse.json +++ b/io.github.flattool.Warehouse.json @@ -1,55 +1,55 @@ { - "id": "io.github.flattool.Warehouse", - "runtime": "org.gnome.Platform", - "runtime-version": "47", - "sdk": "org.gnome.Sdk", - "command": "warehouse", - "finish-args": [ - "--share=ipc", - "--socket=fallback-x11", - "--device=dri", - "--socket=wayland", - "--talk-name=org.freedesktop.Flatpak", - "--filesystem=/var/lib/flatpak/:ro", - "--filesystem=~/.local/share/flatpak/:ro", - "--filesystem=~/.var/app/", - "--filesystem=host-etc" - ], - "cleanup": [ - "/include", - "/lib/pkgconfig", - "/man", - "/share/doc", - "/share/gtk-doc", - "/share/man", - "/share/pkgconfig", - "*.la", - "*.a" - ], - "modules": [ - { - "name": "blueprint-compiler", - "buildsystem": "meson", - "sources": [ - { - "type": "git", - "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", - "tag": "v0.14.0" - } - ], - "cleanup": ["*"] - }, - { - "name": "warehouse", - "builddir": true, - "buildsystem": "meson", - "config-opts": ["-Dprofile=development"], - "sources": [ - { - "type": "dir", - "path": "." - } - ] - } - ] + "id": "io.github.flattool.Warehouse", + "runtime": "org.gnome.Platform", + "runtime-version": "47", + "sdk": "org.gnome.Sdk", + "command": "warehouse", + "finish-args": [ + "--share=ipc", + "--socket=fallback-x11", + "--device=dri", + "--socket=wayland", + "--talk-name=org.freedesktop.Flatpak", + "--filesystem=/var/lib/flatpak/:ro", + "--filesystem=~/.local/share/flatpak/:ro", + "--filesystem=~/.var/app/", + "--filesystem=host-etc" + ], + "cleanup": [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules": [ + { + "name": "blueprint-compiler", + "buildsystem": "meson", + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", + "tag": "v0.14.0" + } + ], + "cleanup": ["*"] + }, + { + "name": "warehouse", + "builddir": true, + "buildsystem": "meson", + "config-opts": ["-Dprofile=development"], + "sources": [ + { + "type": "dir", + "path": "." + } + ] + } + ] } diff --git a/src/change_version_page/change_version_page.py b/src/change_version_page/change_version_page.py index 7ba29bf..76c0999 100644 --- a/src/change_version_page/change_version_page.py +++ b/src/change_version_page/change_version_page.py @@ -7,118 +7,118 @@ import subprocess @Gtk.Template(resource_path="/io/github/flattool/Warehouse/change_version_page/change_version_page.ui") class ChangeVersionPage(Adw.NavigationPage): - __gtype_name__ = 'ChangeVersionPage' - gtc = Gtk.Template.Child - toast_overlay = gtc() - scrolled_window = gtc() - versions_clamp = gtc() - root_group_check_button = gtc() - mask_group = gtc() - mask_row = gtc() - 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'] - 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 = [] - try: - output = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout - lines = output.strip().split('\n') - for line in lines: - line = line.strip().split(": ", 1) - if len(line) < 2: - continue - elif line[0].startswith("Commit"): - commits.append(line[1]) - elif line[0].startswith("Subject"): - changes.append(line[1]) - elif line[0].startswith("Date"): - dates.append(line[1]) - except subprocess.CalledProcessError as cpe: - self.failure = cpe.stderr - return - 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])}") - if commit == self.package.cli_info.get("commit", None): - row.set_sensitive(False) - row.add_prefix(Gtk.Image(icon_name="check-plain-symbolic", margin_start=5, margin_end=5)) - row.set_tooltip_text(_("Currently Installed Version")) - else: - check = Gtk.CheckButton() - check.connect("activate", lambda *_, comm=commit: self.set_commit(comm)) - 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(), - self.package, self.selected_commit, - self.packages_page.changing_version, - self.callback, - 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) + __gtype_name__ = 'ChangeVersionPage' + gtc = Gtk.Template.Child + toast_overlay = gtc() + scrolled_window = gtc() + versions_clamp = gtc() + root_group_check_button = gtc() + mask_group = gtc() + mask_row = gtc() + 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'] + 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 = [] + try: + output = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout + lines = output.strip().split('\n') + for line in lines: + line = line.strip().split(": ", 1) + if len(line) < 2: + continue + elif line[0].startswith("Commit"): + commits.append(line[1]) + elif line[0].startswith("Subject"): + changes.append(line[1]) + elif line[0].startswith("Date"): + dates.append(line[1]) + except subprocess.CalledProcessError as cpe: + self.failure = cpe.stderr + return + 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])}") + if commit == self.package.cli_info.get("commit", None): + row.set_sensitive(False) + row.add_prefix(Gtk.Image(icon_name="check-plain-symbolic", margin_start=5, margin_end=5)) + row.set_tooltip_text(_("Currently Installed Version")) + else: + check = Gtk.CheckButton() + check.connect("activate", lambda *_, comm=commit: self.set_commit(comm)) + 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(), + self.package, self.selected_commit, + self.packages_page.changing_version, + self.callback, + 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) diff --git a/src/const.py.in b/src/const.py.in index 7642fc3..6e8d84e 100644 --- a/src/const.py.in +++ b/src/const.py.in @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only class Config: - DEVEL = '@DEVEL@' == 'Development' - PROFILE = '@DEVEL@' - APP_ID = '@APPID@' - VERSION = '@VERSION@' + DEVEL = '@DEVEL@' == 'Development' + PROFILE = '@DEVEL@' + APP_ID = '@APPID@' + VERSION = '@VERSION@' diff --git a/src/gtk/app_row.py b/src/gtk/app_row.py index d3d8b3f..99d5d33 100644 --- a/src/gtk/app_row.py +++ b/src/gtk/app_row.py @@ -2,43 +2,43 @@ from gi.repository import Adw, Gtk, GLib @Gtk.Template(resource_path="/io/github/flattool/Warehouse/gtk/app_row.ui") class AppRow(Adw.ActionRow): - __gtype_name__ = 'AppRow' - gtc = Gtk.Template.Child - image = gtc() - eol_package_package_status_icon = gtc() - eol_runtime_status_icon = gtc() - pinned_status_icon = gtc() - masked_status_icon = gtc() - check_button = gtc() + __gtype_name__ = 'AppRow' + gtc = Gtk.Template.Child + image = gtc() + eol_package_package_status_icon = gtc() + eol_runtime_status_icon = gtc() + pinned_status_icon = gtc() + masked_status_icon = gtc() + check_button = gtc() - def idle_stuff(self): - if self.package.icon_path: - self.image.add_css_class("icon-dropshadow") - self.image.set_from_file(self.package.icon_path) + def idle_stuff(self): + if self.package.icon_path: + self.image.add_css_class("icon-dropshadow") + self.image.set_from_file(self.package.icon_path) - def gesture_handler(self, *args): - if self.on_long_press: - self.on_long_press(self) + def gesture_handler(self, *args): + if self.on_long_press: + self.on_long_press(self) - def __init__(self, package, on_long_press=None, **kwargs): - super().__init__(**kwargs) + def __init__(self, package, on_long_press=None, **kwargs): + super().__init__(**kwargs) - # Extra Object Creation - self.package = package - self.on_long_press = on_long_press - self.rclick_gesture = Gtk.GestureClick(button=3) - self.long_press_gesture = Gtk.GestureLongPress() + # Extra Object Creation + self.package = package + self.on_long_press = on_long_press + self.rclick_gesture = Gtk.GestureClick(button=3) + self.long_press_gesture = Gtk.GestureLongPress() - # Apply - GLib.idle_add(lambda *_: self.set_title(package.info["name"])) - GLib.idle_add(lambda *_: self.set_subtitle(package.info["id"])) - GLib.idle_add(lambda *_: self.idle_stuff()) - self.add_controller(self.rclick_gesture) - self.add_controller(self.long_press_gesture) - if package.info['id'] == "io.github.flattool.Warehouse": - self.check_button.set_active = lambda *_: None - self.check_button.set_sensitive(False) + # Apply + GLib.idle_add(lambda *_: self.set_title(package.info["name"])) + GLib.idle_add(lambda *_: self.set_subtitle(package.info["id"])) + GLib.idle_add(lambda *_: self.idle_stuff()) + self.add_controller(self.rclick_gesture) + self.add_controller(self.long_press_gesture) + if package.info['id'] == "io.github.flattool.Warehouse": + self.check_button.set_active = lambda *_: None + self.check_button.set_sensitive(False) - # Connections - self.rclick_gesture.connect("released", self.gesture_handler) - self.long_press_gesture.connect("pressed", self.gesture_handler) + # Connections + self.rclick_gesture.connect("released", self.gesture_handler) + self.long_press_gesture.connect("pressed", self.gesture_handler) diff --git a/src/gtk/error_toast.py b/src/gtk/error_toast.py index 75e6251..a9eb972 100644 --- a/src/gtk/error_toast.py +++ b/src/gtk/error_toast.py @@ -1,29 +1,29 @@ from gi.repository import Adw, Gtk, Gdk, GLib class ErrorToast: - main_window = None - def __init__(self, display_msg, error_msg): + 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) + def on_response(dialog, response_id): + if response_id == "copy": + self.clipboard.set(error_msg) - # Extra Object Creation - self.toast = Adw.Toast(title=display_msg, button_label=_("Details")) - popup = Adw.AlertDialog.new(display_msg) - self.clipboard = Gdk.Display.get_default().get_clipboard() + # Extra Object Creation + self.toast = Adw.Toast(title=display_msg, button_label=_("Details")) + popup = Adw.AlertDialog.new(display_msg) + self.clipboard = Gdk.Display.get_default().get_clipboard() - # Apply - print(display_msg) - print(error_msg) - popup.add_response("copy", _("Copy")) - popup.add_response("ok", _("OK")) - lb = Gtk.Label(selectable=True, wrap=True)#, natural_wrap_mode=Gtk.NaturalWrapMode.WORD) - lb.set_markup(f"{GLib.markup_escape_text(error_msg)}") - # lb.set_label(error_msg) - # lb.set_selectable(True) - popup.set_extra_child(lb) + # Apply + print(display_msg) + print(error_msg) + popup.add_response("copy", _("Copy")) + popup.add_response("ok", _("OK")) + lb = Gtk.Label(selectable=True, wrap=True)#, natural_wrap_mode=Gtk.NaturalWrapMode.WORD) + lb.set_markup(f"{GLib.markup_escape_text(error_msg)}") + # lb.set_label(error_msg) + # lb.set_selectable(True) + popup.set_extra_child(lb) - # Connections - self.toast.connect("button-clicked", lambda *_: popup.present(self.main_window)) - popup.connect("response", on_response) + # Connections + self.toast.connect("button-clicked", lambda *_: popup.present(self.main_window)) + popup.connect("response", on_response) diff --git a/src/host_info.py b/src/host_info.py index 9cac28d..01cf4d0 100644 --- a/src/host_info.py +++ b/src/host_info.py @@ -9,376 +9,376 @@ direction = Gtk.Image().get_direction() class Flatpak: - def open_app(self, callback=None): - self.failed_app_run = None - def thread(*args): - if self.is_runtime: - self.failed_app_run = "error: cannot open a runtime" - try: - subprocess.run(['flatpak-spawn', '--host', 'flatpak', 'run', f"{self.info['ref']}"], capture_output=True, text=True, check=True) - except subprocess.CalledProcessError as cpe: - self.failed_app_run = cpe - except Exception as e: - self.failed_app_run = e + def open_app(self, callback=None): + self.failed_app_run = None + def thread(*args): + if self.is_runtime: + self.failed_app_run = "error: cannot open a runtime" + try: + subprocess.run(['flatpak-spawn', '--host', 'flatpak', 'run', f"{self.info['ref']}"], capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as cpe: + self.failed_app_run = cpe + except Exception as e: + self.failed_app_run = e - Gio.Task.new(None, None, callback).run_in_thread(thread) + Gio.Task.new(None, None, callback).run_in_thread(thread) - def open_data(self): - if not os.path.exists(self.data_path): - return f"Path '{self.data_path}' does not exist" - try: - Gio.AppInfo.launch_default_for_uri(f"file://{self.data_path}", None) - except GLib.GError as e: - return e + def open_data(self): + if not os.path.exists(self.data_path): + return f"Path '{self.data_path}' does not exist" + try: + Gio.AppInfo.launch_default_for_uri(f"file://{self.data_path}", None) + except GLib.GError as e: + return e - def get_data_size(self, callback=None): - size = [None] - def thread(*args): - sed = "sed 's/K/ KB/; s/M/ MB/; s/G/ GB/; s/T/ TB/; s/P/ PB/;'" - size[0] = subprocess.run(['sh', '-c', f"du -sh {self.data_path} | {sed}"], capture_output=True, text=True).stdout.split("\t")[0] - def on_done(*arg): - if callback: - callback(f"~ {size[0]}") - Gio.Task.new(None, None, on_done).run_in_thread(thread) + def get_data_size(self, callback=None): + size = [None] + def thread(*args): + sed = "sed 's/K/ KB/; s/M/ MB/; s/G/ GB/; s/T/ TB/; s/P/ PB/;'" + size[0] = subprocess.run(['sh', '-c', f"du -sh {self.data_path} | {sed}"], capture_output=True, text=True).stdout.split("\t")[0] + def on_done(*arg): + if callback: + callback(f"~ {size[0]}") + Gio.Task.new(None, None, on_done).run_in_thread(thread) - def trash_data(self, callback=None): - try: - subprocess.run(['gio', 'trash', self.data_path], capture_output=True, text=True, check=True) - except subprocess.CalledProcessError as cpe: - raise cpe - except Exception as e: - raise e + def trash_data(self, callback=None): + try: + subprocess.run(['gio', 'trash', self.data_path], capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as cpe: + raise cpe + except Exception as e: + raise e - def set_mask(self, should_mask, callback=None): - self.failed_mask = None - def thread(*args): - cmd = ['flatpak-spawn', '--host', 'flatpak', 'mask', self.info["id"]] - installation = self.info["installation"] - if installation == "user" or installation == "system": - 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 - except subprocess.CalledProcessError as cpe: - self.failed_mask = cpe - except Exception as e: - self.failed_mask = e + def set_mask(self, should_mask, callback=None): + self.failed_mask = None + def thread(*args): + cmd = ['flatpak-spawn', '--host', 'flatpak', 'mask', self.info["id"]] + installation = self.info["installation"] + if installation == "user" or installation == "system": + 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 + except subprocess.CalledProcessError as cpe: + self.failed_mask = cpe + except Exception as e: + self.failed_mask = e - Gio.Task.new(None, None, callback).run_in_thread(thread) + Gio.Task.new(None, None, callback).run_in_thread(thread) - def set_pin(self, should_pin, callback=None): - 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"] - if installation == "user" or installation == "system": - cmd.append(f"--{installation}") - else: - cmd.append(f"--installation={installation}") + def set_pin(self, should_pin, callback=None): + 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"] + if installation == "user" or installation == "system": + cmd.append(f"--{installation}") + else: + cmd.append(f"--installation={installation}") - if not should_pin: - cmd.append("--remove") + if not should_pin: + cmd.append("--remove") - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as cpe: - self.failed_pin = cpe - except Exception as e: - self.failed_mask = e + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as cpe: + self.failed_pin = cpe + except Exception as e: + self.failed_mask = e - Gio.Task.new(None, None, callback).run_in_thread(thread) + Gio.Task.new(None, None, callback).run_in_thread(thread) - 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: - callee_callback() + 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: + callee_callback() - def thread(*args): - HostInfo.main_window.add_refresh_lockout("uninstalling packages") - cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y', self.info["ref"]] - installation = self.info["installation"] - if installation == "system" or installation == "user": - cmd.append(f"--{installation}") - else: - cmd.append(f"--installation={installation}") + def thread(*args): + HostInfo.main_window.add_refresh_lockout("uninstalling packages") + cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y', self.info["ref"]] + installation = self.info["installation"] + if installation == "system" or installation == "user": + cmd.append(f"--{installation}") + else: + cmd.append(f"--installation={installation}") - try: - subprocess.run(cmd, check=True, text=True, capture_output=True) - except subprocess.CalledProcessError as cpe: - self.failed_uninstall = cpe - except Exception as e: - self.failed_uninstall = e + try: + subprocess.run(cmd, check=True, text=True, capture_output=True) + except subprocess.CalledProcessError as cpe: + self.failed_uninstall = cpe + except Exception as e: + self.failed_uninstall = e - Gio.Task.new(None, None, callback).run_in_thread(thread) + Gio.Task.new(None, None, callback).run_in_thread(thread) - def get_cli_info(self): - cli_info = {} - cmd = "LC_ALL=C flatpak info " - installation = self.info["installation"] + def get_cli_info(self): + cli_info = {} + cmd = "LC_ALL=C flatpak info " + installation = self.info["installation"] - if installation == "user": - cmd += "--user " - elif installation == "system": - cmd += "--system " - else: - cmd += f"--installation={installation} " + if installation == "user": + cmd += "--user " + elif installation == "system": + cmd += "--system " + else: + cmd += f"--installation={installation} " - cmd += self.info["ref"] - try: - output = subprocess.run( - ['flatpak-spawn', '--host', 'sh', '-c', cmd], - text=True, capture_output=True - ).stdout - except Exception as e: - raise e + cmd += self.info["ref"] + try: + output = subprocess.run( + ['flatpak-spawn', '--host', 'sh', '-c', cmd], + text=True, capture_output=True + ).stdout + except Exception as e: + raise e - lines = output.strip().split("\n") - cli_info["description"] = "" - 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 + lines = output.strip().split("\n") + cli_info["description"] = "" + 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 - word[0] = word[0].lower() - if "installed" in word[0]: - word[1] = word[1].replace("?", " ") - cli_info[word[0]] = word[1] + word[0] = word[0].lower() + if "installed" in word[0]: + word[1] = word[1].replace("?", " ") + cli_info[word[0]] = word[1] - self.cli_info = cli_info - return cli_info + self.cli_info = cli_info + return cli_info - def __init__(self, columns): - self.info = { - "name": columns[0], - "id": columns[1], - "version": columns[2], - "branch": columns[3], - "arch": columns[4], - "origin": columns[5], - "installation": columns[6], - "ref": columns[7], - "installed_size": columns[8], - "options": columns[9], - } - self.is_runtime = "runtime" in self.info["options"] - self.data_path = f"{home}/.var/app/{self.info["id"]}" - self.data_size = -1 - self.cli_info = None - installation = self.info["installation"] - if len(i := installation.split(' ')) > 1: - self.info["installation"] = i[1].replace("(", "").replace(")", "") - else: - self.info["installation"] = installation + def __init__(self, columns): + self.info = { + "name": columns[0], + "id": columns[1], + "version": columns[2], + "branch": columns[3], + "arch": columns[4], + "origin": columns[5], + "installation": columns[6], + "ref": columns[7], + "installed_size": columns[8], + "options": columns[9], + } + self.is_runtime = "runtime" in self.info["options"] + self.data_path = f"{home}/.var/app/{self.info["id"]}" + self.data_size = -1 + self.cli_info = None + installation = self.info["installation"] + if len(i := installation.split(' ')) > 1: + self.info["installation"] = i[1].replace("(", "").replace(")", "") + else: + self.info["installation"] = installation - self.is_eol = "eol=" in self.info["options"] - self.dependent_runtime = None - self.failed_app_run = None - self.failed_mask = None - self.failed_uninstall = None - self.app_row = None + self.is_eol = "eol=" in self.info["options"] + self.dependent_runtime = None + self.failed_app_run = None + self.failed_mask = None + self.failed_uninstall = None + self.app_row = None - try: - self.is_masked = self.info["id"] in HostInfo.masks[self.info["installation"]] - except KeyError: - self.is_masked = False + try: + self.is_masked = self.info["id"] in HostInfo.masks[self.info["installation"]] + except KeyError: + self.is_masked = False - try: - self.is_pinned = f"runtime/{self.info['ref']}" in HostInfo.pins[self.info["installation"]] - except KeyError: - self.is_pinned = False + try: + self.is_pinned = f"runtime/{self.info['ref']}" in HostInfo.pins[self.info["installation"]] + except KeyError: + self.is_pinned = False - try: - self.icon_path = ( - icon_theme.lookup_icon( - self.info["id"], None, 512, 1, direction, 0 - ) - .get_file() - .get_path() - ) - except GLib.GError as e: - print(f"Minor error in looking up icon for {self.info['id']}", e) - self.icon_path = None + try: + self.icon_path = ( + icon_theme.lookup_icon( + self.info["id"], None, 512, 1, direction, 0 + ) + .get_file() + .get_path() + ) + except GLib.GError as e: + print(f"Minor error in looking up icon for {self.info['id']}", e) + self.icon_path = None class Remote: - def __init__(self, name, title, disabled): - self.name = name - self.title = title - self.disabled = disabled - if title == "" or title == "-": - self.title = name + def __init__(self, name, title, disabled): + self.name = name + self.title = title + self.disabled = disabled + if title == "" or title == "-": + self.title = name class HostInfo: - home = home - clipboard = Gdk.Display.get_default().get_clipboard() - main_window = None - snapshots_path = f"{home}/.var/app/io.github.flattool.Warehouse/data/Snapshots/" + home = home + clipboard = Gdk.Display.get_default().get_clipboard() + main_window = None + snapshots_path = f"{home}/.var/app/io.github.flattool.Warehouse/data/Snapshots/" - # Get all possible installation icon theme dirs - output = subprocess.run( - ['flatpak-spawn', '--host', - 'flatpak', '--installations'], - text=True, - capture_output=True, - ).stdout - 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 = {} - remotes = {} - installations = [] - masks = {} - pins = {} - dependent_runtime_refs = [] - @classmethod - def get_flatpaks(this, callback=None): - # Callback is a function to run after the host flatpaks are found - this.flatpaks.clear() - this.id_to_flatpak.clear() - this.ref_to_flatpak.clear() - this.remotes.clear() - this.installations.clear() - this.masks.clear() - this.pins.clear() - this.dependent_runtime_refs.clear() + # Get all possible installation icon theme dirs + output = subprocess.run( + ['flatpak-spawn', '--host', + 'flatpak', '--installations'], + text=True, + capture_output=True, + ).stdout + 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 = {} + remotes = {} + installations = [] + masks = {} + pins = {} + dependent_runtime_refs = [] + @classmethod + def get_flatpaks(this, callback=None): + # Callback is a function to run after the host flatpaks are found + this.flatpaks.clear() + this.id_to_flatpak.clear() + this.ref_to_flatpak.clear() + this.remotes.clear() + this.installations.clear() + this.masks.clear() + this.pins.clear() + this.dependent_runtime_refs.clear() - def thread(task, *args): + def thread(task, *args): - # Remotes - def remote_info(installation): - cmd = ['flatpak-spawn', '--host', - 'flatpak', 'remotes', '--columns=name,title,options', '--show-disabled'] - if installation == "user" or installation == "system": - cmd.append(f"--{installation}") - else: - cmd.append(f"--installation={installation}") - output = subprocess.run( - cmd, text=True, - capture_output=True, - ).stdout - lines = output.strip().split("\n") - remote_list = [] - if lines[0] != '': - for line in lines: - line = line.split("\t") - remote_list.append(Remote(name=line[0], title=line[1], disabled=(len(line) == 3) and "disabled" in line[2])) - this.remotes[installation] = remote_list + # Remotes + def remote_info(installation): + cmd = ['flatpak-spawn', '--host', + 'flatpak', 'remotes', '--columns=name,title,options', '--show-disabled'] + if installation == "user" or installation == "system": + cmd.append(f"--{installation}") + else: + cmd.append(f"--installation={installation}") + output = subprocess.run( + cmd, text=True, + capture_output=True, + ).stdout + lines = output.strip().split("\n") + remote_list = [] + if lines[0] != '': + for line in lines: + line = line.split("\t") + remote_list.append(Remote(name=line[0], title=line[1], disabled=(len(line) == 3) and "disabled" in line[2])) + this.remotes[installation] = remote_list - # Masks - cmd = ['flatpak-spawn', '--host', - 'flatpak', 'mask',] - if installation == "user" or installation == "system": - cmd.append(f"--{installation}") - else: - cmd.append(f"--installation={installation}") - output = subprocess.run( - cmd, text=True, - capture_output=True, - ).stdout - lines = output.strip().replace(" ", "").split("\n") - if lines[0] != '': - this.masks[installation] = lines + # Masks + cmd = ['flatpak-spawn', '--host', + 'flatpak', 'mask',] + if installation == "user" or installation == "system": + cmd.append(f"--{installation}") + else: + cmd.append(f"--installation={installation}") + output = subprocess.run( + cmd, text=True, + capture_output=True, + ).stdout + lines = output.strip().replace(" ", "").split("\n") + if lines[0] != '': + this.masks[installation] = lines - # Pins - cmd = ['flatpak-spawn', '--host', - 'flatpak', 'pin',] - if installation == "user" or installation == "system": - cmd.append(f"--{installation}") - else: - cmd.append(f"--installation={installation}") - output = subprocess.run( - cmd, text=True, - capture_output=True, - ).stdout - lines = output.strip().replace(" ", "").split("\n") - if lines[0] != '': - this.pins[installation] = lines + # Pins + cmd = ['flatpak-spawn', '--host', + 'flatpak', 'pin',] + if installation == "user" or installation == "system": + cmd.append(f"--{installation}") + else: + cmd.append(f"--installation={installation}") + output = subprocess.run( + cmd, text=True, + capture_output=True, + ).stdout + lines = output.strip().replace(" ", "").split("\n") + if lines[0] != '': + this.pins[installation] = lines - try: - # Installations - # Get all config files for any extra installations - custom_install_config_path = "/run/host/etc/flatpak/installations.d" - if os.path.exists(custom_install_config_path): - for file in os.listdir(custom_install_config_path): - with open(f"{custom_install_config_path}/{file}", "r") as f: - for line in f: - if line.startswith("[Installation"): - # Get specifically the installation name itself - this.installations.append(line.replace("[Installation \"", "").replace("\"]", "").strip()) + try: + # Installations + # Get all config files for any extra installations + custom_install_config_path = "/run/host/etc/flatpak/installations.d" + if os.path.exists(custom_install_config_path): + for file in os.listdir(custom_install_config_path): + with open(f"{custom_install_config_path}/{file}", "r") as f: + for line in f: + if line.startswith("[Installation"): + # Get specifically the installation name itself + this.installations.append(line.replace("[Installation \"", "").replace("\"]", "").strip()) - this.installations.append("user") - this.installations.append("system") - for i in this.installations: - remote_info(i) - remote_info("user") - remote_info("system") + this.installations.append("user") + this.installations.append("system") + for i in this.installations: + remote_info(i) + remote_info("user") + remote_info("system") - # Packages - output = subprocess.run( - ['flatpak-spawn', '--host', 'flatpak', 'list', - '--columns=name,application,version,branch,arch,origin,installation,ref,size,options'], - text=True, check=True, - capture_output=True, - ).stdout - lines = output.strip().split("\n") - for i in lines: - package = Flatpak(i.split("\t")) - 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', - 'flatpak', 'list', '--columns=runtime,ref'], - text=True, check=True, - capture_output=True, - ).stdout - lines = output.split("\n") - for index, line in enumerate(lines): - 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) + # Packages + output = subprocess.run( + ['flatpak-spawn', '--host', 'flatpak', 'list', + '--columns=name,application,version,branch,arch,origin,installation,ref,size,options'], + text=True, check=True, + capture_output=True, + ).stdout + lines = output.strip().split("\n") + for i in lines: + package = Flatpak(i.split("\t")) + 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', + 'flatpak', 'list', '--columns=runtime,ref'], + text=True, check=True, + capture_output=True, + ).stdout + lines = output.split("\n") + for index, line in enumerate(lines): + 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) diff --git a/src/packages_page/filters_page.blp b/src/packages_page/filters_page.blp index 80c88fa..8d0761f 100644 --- a/src/packages_page/filters_page.blp +++ b/src/packages_page/filters_page.blp @@ -2,117 +2,117 @@ using Gtk 4.0; using Adw 1; template $FiltersPage : Adw.NavigationPage { - title: _("Filter Packages"); - Adw.ToolbarView { - [top] - Adw.HeaderBar {} - ScrolledWindow { - Adw.Clamp { - Box { - margin-start: 12; - margin-end: 12; - margin-top: 12; - margin-bottom: 12; - spacing: 24; - orientation: vertical; - halign: fill; - Adw.PreferencesGroup { - title: _("Filter by Package Type"); - description: _("Show packages of these types"); - Adw.ActionRow application_row { - title: _("Applications"); - subtitle: _("Packages that can be opened"); - CheckButton app_check {} - activatable-widget: app_check; - } - Adw.ActionRow runtime_row { - title: _("Runtimes"); - subtitle: _("Packages that applications depend on"); - CheckButton runtime_check {} - activatable-widget: runtime_check; - } - } + title: _("Filter Packages"); + Adw.ToolbarView { + [top] + Adw.HeaderBar {} + ScrolledWindow { + Adw.Clamp { + Box { + margin-start: 12; + margin-end: 12; + margin-top: 12; + margin-bottom: 12; + spacing: 24; + orientation: vertical; + halign: fill; + Adw.PreferencesGroup { + title: _("Filter by Package Type"); + description: _("Show packages of these types"); + Adw.ActionRow application_row { + title: _("Applications"); + subtitle: _("Packages that can be opened"); + CheckButton app_check {} + activatable-widget: app_check; + } + Adw.ActionRow runtime_row { + title: _("Runtimes"); + subtitle: _("Packages that applications depend on"); + CheckButton runtime_check {} + activatable-widget: runtime_check; + } + } - Adw.PreferencesGroup remotes_group { - title: _("Filter by Remotes"); - description: _("Show packages from selected remotes"); - header-suffix: - Switch all_remotes_switch { - valign: center; - } - ; - Adw.ActionRow { - visible: bind all_remotes_switch.active inverted; - [child] - Box { - spacing: 3; - orientation: vertical; - Label { - margin-top: 7; - label: _("Showing packages from all remotes"); - wrap: true; - halign: center; - styles ["heading"] - } - Label { - label: _("Enable to show packages from selected remotes"); - margin-start: 16; - margin-end: 16; - margin-bottom: 8; - justify: center; - halign: center; - wrap: true; - } - } - } - } + Adw.PreferencesGroup remotes_group { + title: _("Filter by Remotes"); + description: _("Show packages from selected remotes"); + header-suffix: + Switch all_remotes_switch { + valign: center; + } + ; + Adw.ActionRow { + visible: bind all_remotes_switch.active inverted; + [child] + Box { + spacing: 3; + orientation: vertical; + Label { + margin-top: 7; + label: _("Showing packages from all remotes"); + wrap: true; + halign: center; + styles ["heading"] + } + Label { + label: _("Enable to show packages from selected remotes"); + margin-start: 16; + margin-end: 16; + margin-bottom: 8; + justify: center; + halign: center; + wrap: true; + } + } + } + } - Adw.PreferencesGroup runtimes_group { - title: _("Filter by Runtimes"); - description: _("Show apps using selected runtimes"); - header-suffix: - Switch all_runtimes_switch { - valign: center; - } - ; - Adw.ActionRow { - visible: bind all_runtimes_switch.active inverted; - [child] - Box { - spacing: 3; - orientation: vertical; - Label { - margin-top: 7; - label: _("Showing apps using any runtime"); - wrap: true; - halign: center; - styles ["heading"] - } - Label { - label: _("Enable to show apps using selected runtimes"); - margin-start: 16; - margin-end: 16; - margin-bottom: 8; - justify: center; - halign: center; - wrap: true; - } - } - } - } - } - } - } - [bottom] - ActionBar action_bar { - [center] - Button reset_button { - sensitive: bind action_bar.revealed; - margin-top: 3; - margin-bottom: 3; - label: _("Reset Filters"); - styles ["pill"] - } - } - } + Adw.PreferencesGroup runtimes_group { + title: _("Filter by Runtimes"); + description: _("Show apps using selected runtimes"); + header-suffix: + Switch all_runtimes_switch { + valign: center; + } + ; + Adw.ActionRow { + visible: bind all_runtimes_switch.active inverted; + [child] + Box { + spacing: 3; + orientation: vertical; + Label { + margin-top: 7; + label: _("Showing apps using any runtime"); + wrap: true; + halign: center; + styles ["heading"] + } + Label { + label: _("Enable to show apps using selected runtimes"); + margin-start: 16; + margin-end: 16; + margin-bottom: 8; + justify: center; + halign: center; + wrap: true; + } + } + } + } + } + } + } + [bottom] + ActionBar action_bar { + [center] + Button reset_button { + sensitive: bind action_bar.revealed; + margin-top: 3; + margin-bottom: 3; + label: _("Reset Filters"); + styles ["pill"] + } + } + } } diff --git a/src/packages_page/filters_page.py b/src/packages_page/filters_page.py index 5fc58cf..e062666 100644 --- a/src/packages_page/filters_page.py +++ b/src/packages_page/filters_page.py @@ -2,195 +2,195 @@ from gi.repository import Adw, Gtk, Gio from .host_info import HostInfo class FilterRow(Adw.ActionRow): - __gtype_name__ = 'FilterRow' - def __init__(self, item=None, installation=None, **kwargs): - super().__init__(**kwargs) - self.item = item - self.installation = installation - self.check_button = Gtk.CheckButton() - self.add_suffix(self.check_button) - self.set_activatable_widget(self.check_button) + __gtype_name__ = 'FilterRow' + def __init__(self, item=None, installation=None, **kwargs): + super().__init__(**kwargs) + self.item = item + self.installation = installation + self.check_button = Gtk.CheckButton() + self.add_suffix(self.check_button) + self.set_activatable_widget(self.check_button) @Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/filters_page.ui") class FiltersPage(Adw.NavigationPage): - __gtype_name__ = 'FiltersPage' - gtc = Gtk.Template.Child - app_check = gtc() - runtime_check = gtc() - remotes_group = gtc() - all_remotes_switch = gtc() - runtimes_group = gtc() - all_runtimes_switch = gtc() - action_bar = gtc() - reset_button = gtc() + __gtype_name__ = 'FiltersPage' + gtc = Gtk.Template.Child + app_check = gtc() + runtime_check = gtc() + remotes_group = gtc() + all_remotes_switch = gtc() + runtimes_group = gtc() + all_runtimes_switch = gtc() + action_bar = gtc() + reset_button = gtc() - remote_rows = [] - runtime_rows = [] + remote_rows = [] + runtime_rows = [] - def reset_filters(self): - self.settings.reset("show-apps") - self.settings.reset("show-runtimes") - self.settings.reset("remotes-list") - self.settings.reset("runtimes-list") - self.generate_filters() - self.packages_page.apply_filters() + def reset_filters(self): + self.settings.reset("show-apps") + self.settings.reset("show-runtimes") + self.settings.reset("remotes-list") + self.settings.reset("runtimes-list") + self.generate_filters() + self.packages_page.apply_filters() - def is_defaulted(self): - default = True - if not self.app_check.get_active(): - default = False - if self.runtime_check.get_active(): - default = False - if self.all_remotes_switch.get_active(): - default = False - if self.all_runtimes_switch.get_active(): - default = False - self.action_bar.set_revealed(not default) + def is_defaulted(self): + default = True + if not self.app_check.get_active(): + default = False + if self.runtime_check.get_active(): + default = False + if self.all_remotes_switch.get_active(): + default = False + if self.all_runtimes_switch.get_active(): + default = False + self.action_bar.set_revealed(not default) - def update_gsettings(self): - self.is_defaulted() - if not self.is_settings_settable: - return - self.settings.set_boolean("show-apps", self.show_apps) - self.settings.set_boolean("show-runtimes", self.show_runtimes) - self.settings.set_string("remotes-list", self.remotes_string) - self.settings.set_string("runtimes-list", self.runtimes_string) - self.packages_page.apply_filters() + def update_gsettings(self): + self.is_defaulted() + if not self.is_settings_settable: + return + self.settings.set_boolean("show-apps", self.show_apps) + self.settings.set_boolean("show-runtimes", self.show_runtimes) + self.settings.set_string("remotes-list", self.remotes_string) + self.settings.set_string("runtimes-list", self.runtimes_string) + self.packages_page.apply_filters() - 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() + 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() - def all_remotes_handler(self, switch, state): - self.remotes_string = "" - if not state: - self.remotes_string = "all" + def all_remotes_handler(self, switch, state): + self.remotes_string = "" + if not state: + self.remotes_string = "all" - for row in self.remote_rows: - row.set_visible(state) - if state and row.check_button.get_active(): - self.remotes_string += f"{row.item.name}<>{row.installation};" - elif state: - self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "") - - self.update_gsettings() + for row in self.remote_rows: + row.set_visible(state) + if state and row.check_button.get_active(): + 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 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): - if row.check_button.get_active(): - self.remotes_string += f"{row.item.name}<>{row.installation};" - else: - self.remotes_string = self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "") - self.update_gsettings() + def remote_row_check_handler(self, row): + if row.check_button.get_active(): + self.remotes_string += f"{row.item.name}<>{row.installation};" + else: + self.remotes_string = self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "") + self.update_gsettings() - def runtime_row_check_handler(self, row): - if row.check_button.get_active(): - self.runtimes_string += f"{row.item};" - else: - self.runtimes_string = self.runtimes_string.replace(f"{row.item};", "") - self.update_gsettings() + def runtime_row_check_handler(self, row): + if row.check_button.get_active(): + self.runtimes_string += f"{row.item};" + else: + self.runtimes_string = self.runtimes_string.replace(f"{row.item};", "") + self.update_gsettings() - def generate_remote_filters(self): - for row in self.remote_rows: - self.remotes_group.remove(row) + def generate_remote_filters(self): + for row in self.remote_rows: + self.remotes_group.remove(row) - self.remote_rows.clear() - for installation, remotes in HostInfo.remotes.items(): - for remote in remotes: - if remote.disabled: - continue + self.remote_rows.clear() + for installation, remotes in HostInfo.remotes.items(): + for remote in remotes: + if remote.disabled: + continue - row = FilterRow(remote, installation) - row.set_title(remote.title) - row.set_subtitle(_("Installation: {}").format(installation)) - row.check_button.set_active(f"{remote.name}<>{installation}" in self.remotes_string) - row.check_button.connect("toggled", lambda *_, row=row: self.remote_row_check_handler(row)) - row.set_visible(self.all_remotes_switch.get_active()) - self.remote_rows.append(row) - self.remotes_group.add(row) + row = FilterRow(remote, installation) + row.set_title(remote.title) + row.set_subtitle(_("Installation: {}").format(installation)) + row.check_button.set_active(f"{remote.name}<>{installation}" in self.remotes_string) + row.check_button.connect("toggled", lambda *_, row=row: self.remote_row_check_handler(row)) + row.set_visible(self.all_remotes_switch.get_active()) + self.remote_rows.append(row) + self.remotes_group.add(row) - self.remotes_group.set_visible(len(self.remote_rows) > 1) - self.all_remotes_switch.set_active("all" != self.remotes_string) + self.remotes_group.set_visible(len(self.remote_rows) > 1) + self.all_remotes_switch.set_active("all" != self.remotes_string) - def generate_runtime_filters(self): - for row in self.runtime_rows: - self.runtimes_group.remove(row) - self.runtime_rows.clear() - if len(HostInfo.dependent_runtime_refs) < 2: - self.runtimes_group.set_visible(False) - if self.runtimes_string != "all": - self.runtimes_string = "all" - self.settings.set_string("runtimes-list", self.runtimes_string) - self.packages_page.apply_filters() + def generate_runtime_filters(self): + for row in self.runtime_rows: + self.runtimes_group.remove(row) + self.runtime_rows.clear() + if len(HostInfo.dependent_runtime_refs) < 2: + self.runtimes_group.set_visible(False) + if self.runtimes_string != "all": + self.runtimes_string = "all" + self.settings.set_string("runtimes-list", self.runtimes_string) + self.packages_page.apply_filters() - return + return - for j, ref in enumerate(HostInfo.dependent_runtime_refs): - row = FilterRow(ref) - row.set_title(ref) - row.check_button.set_active(ref in self.runtimes_string) - row.check_button.connect("toggled", lambda *_, row=row: self.runtime_row_check_handler(row)) - row.set_visible(self.all_runtimes_switch.get_active()) - self.runtime_rows.append(row) - self.runtimes_group.add(row) + for j, ref in enumerate(HostInfo.dependent_runtime_refs): + row = FilterRow(ref) + row.set_title(ref) + row.check_button.set_active(ref in self.runtimes_string) + row.check_button.connect("toggled", lambda *_, row=row: self.runtime_row_check_handler(row)) + row.set_visible(self.all_runtimes_switch.get_active()) + self.runtime_rows.append(row) + self.runtimes_group.add(row) - self.runtimes_group.set_visible(len(self.runtime_rows) > 1) - self.all_runtimes_switch.set_active("all" != self.runtimes_string) + self.runtimes_group.set_visible(len(self.runtime_rows) > 1) + self.all_runtimes_switch.set_active("all" != self.runtimes_string) - def generate_filters(self): - self.is_settings_settable = False - self.show_apps = self.settings.get_boolean("show-apps") - 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) + def generate_filters(self): + self.is_settings_settable = False + self.show_apps = self.settings.get_boolean("show-apps") + 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) - self.generate_remote_filters() - self.generate_runtime_filters() + self.generate_remote_filters() + self.generate_runtime_filters() - self.is_settings_settable = True + self.is_settings_settable = True - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) - # Extra Objects Creation - self.packages_page = None # To be set in packages page - self.main_window = HostInfo.main_window - self.settings = Gio.Settings.new("io.github.flattool.Warehouse.filter") - self.is_settings_settable = False - self.show_apps = self.settings.get_boolean("show-apps") - 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") + # Extra Objects Creation + self.packages_page = None # To be set in packages page + self.main_window = HostInfo.main_window + self.settings = Gio.Settings.new("io.github.flattool.Warehouse.filter") + self.is_settings_settable = False + self.show_apps = self.settings.get_boolean("show-apps") + 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") - # Apply - if "," in self.runtimes_string: - # Convert Warehouse 1.X runtimes filter string from , to ; for item seperationg - self.runtimes_string = self.runtimes_string.replace(",", ";") - self.settings.set_string("runtimes-list", self.runtimes_string) + # Apply + if "," in self.runtimes_string: + # Convert Warehouse 1.X runtimes filter string from , to ; for item seperationg + self.runtimes_string = self.runtimes_string.replace(",", ";") + self.settings.set_string("runtimes-list", self.runtimes_string) - # Connections - self.app_check.connect("toggled", self.app_check_handler) - self.runtime_check.connect("toggled", self.runtime_check_handler) - self.all_remotes_switch.connect("state-set", self.all_remotes_handler) - self.all_runtimes_switch.connect("state-set", self.all_runtimes_handler) - self.reset_button.connect("clicked", lambda *_: self.reset_filters()) + # Connections + self.app_check.connect("toggled", self.app_check_handler) + self.runtime_check.connect("toggled", self.runtime_check_handler) + self.all_remotes_switch.connect("state-set", self.all_remotes_handler) + self.all_runtimes_switch.connect("state-set", self.all_runtimes_handler) + self.reset_button.connect("clicked", lambda *_: self.reset_filters()) diff --git a/src/packages_page/packages_page.blp b/src/packages_page/packages_page.blp index 1513fdc..b941c4d 100644 --- a/src/packages_page/packages_page.blp +++ b/src/packages_page/packages_page.blp @@ -2,184 +2,184 @@ using Gtk 4.0; using Adw 1; template $PackagesPage : Adw.BreakpointBin { - width-request: 1; + width-request: 1; height-request: 1; - Adw.Breakpoint packages_bpt { + Adw.Breakpoint packages_bpt { condition ("max-width: 600") setters { packages_split.collapsed: true; - packages_split.show-content: false; - content_stack.transition-duration: 9999999; - reset_filters_button.visible: true; + packages_split.show-content: false; + content_stack.transition-duration: 9999999; + reset_filters_button.visible: true; } } - Adw.NavigationPage { - title: _("Packages"); - Stack stack { - Adw.ToolbarView loading_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.ToolbarView uninstalling_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.ToolbarView reinstalling_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.ToolbarView changing_version_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.NavigationSplitView packages_split { - sidebar-width-fraction: 0.5; - max-sidebar-width: 999999999; - sidebar: - Adw.NavigationPage packages_navpage { - title: _("Packages"); - Adw.ToastOverlay packages_toast_overlay { - Adw.ToolbarView packages_tbv { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - [start] - ToggleButton search_button { - icon-name: "loupe-large-symbolic"; - tooltip-text: _("Search Packages"); - } - [end] - ToggleButton filter_button { - icon-name: "funnel-symbolic"; - tooltip-text: _("Filter Packages"); - } - [end] - ToggleButton select_button { - icon-name: "selection-mode-symbolic"; - tooltip-text: _("Select Packages"); - } - } - [top] - SearchBar search_bar { - search-mode-enabled: bind search_button.active bidirectional; - SearchEntry search_entry { - hexpand: true; - placeholder-text: _("Search Packages"); - } - } - Stack status_stack { - ScrolledWindow scrolled_window { - ListBox packages_list_box { - styles ["navigation-sidebar"] - } - } - Adw.StatusPage no_filter_results { - title: _("No Packages Match Filters"); - description: _("No installed package matches all of the currently applied filters"); - icon-name: "funnel-symbolic"; - Button reset_filters_button { - label: _("Reset Filters"); - halign: center; - visible: false; - styles ["pill"] - } - } - Adw.StatusPage no_packages { - title: _("No Packages Found"); - description: _("Warehouse cannot see the list of installed packages or your system has no packages installed"); - icon-name: "error-symbolic"; - } - Adw.StatusPage no_results { - title: _("No Results Found"); - description: _("Try a different search"); - icon-name: "system-search-symbolic"; - } - } - [bottom] - Revealer { - reveal-child: bind select_button.active; - transition-type: slide_up; - [center] - Box bottom_bar { - styles ["toolbar"] - hexpand: true; - homogeneous: true; - Button select_all_button { - styles ["raised"] - Adw.ButtonContent { - icon-name: "selection-mode-symbolic"; - label: _("Select All"); - can-shrink: true; - } - } - MenuButton copy_button { - styles ["raised"] - Adw.ButtonContent { - icon-name: "edit-copy-symbolic"; - label: _("Copy"); - can-shrink: true; - } - popover: copy_pop; - } - Button uninstall_button { - styles ["raised"] - Adw.ButtonContent { - icon-name: "user-trash-symbolic"; - label: _("Uninstall"); - can-shrink: true; - } - } - } - } - } - } - } - ; - content: - Adw.NavigationPage { - title: "Content Stack"; - Stack content_stack { - transition-type: slide_left_right; - $PropertiesPage properties_page {} - $FiltersPage filters_page {} - } - } - ; - } - } - } + Adw.NavigationPage { + title: _("Packages"); + Stack stack { + Adw.ToolbarView loading_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.ToolbarView uninstalling_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.ToolbarView reinstalling_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.ToolbarView changing_version_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.NavigationSplitView packages_split { + sidebar-width-fraction: 0.5; + max-sidebar-width: 999999999; + sidebar: + Adw.NavigationPage packages_navpage { + title: _("Packages"); + Adw.ToastOverlay packages_toast_overlay { + Adw.ToolbarView packages_tbv { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + [start] + ToggleButton search_button { + icon-name: "loupe-large-symbolic"; + tooltip-text: _("Search Packages"); + } + [end] + ToggleButton filter_button { + icon-name: "funnel-symbolic"; + tooltip-text: _("Filter Packages"); + } + [end] + ToggleButton select_button { + icon-name: "selection-mode-symbolic"; + tooltip-text: _("Select Packages"); + } + } + [top] + SearchBar search_bar { + search-mode-enabled: bind search_button.active bidirectional; + SearchEntry search_entry { + hexpand: true; + placeholder-text: _("Search Packages"); + } + } + Stack status_stack { + ScrolledWindow scrolled_window { + ListBox packages_list_box { + styles ["navigation-sidebar"] + } + } + Adw.StatusPage no_filter_results { + title: _("No Packages Match Filters"); + description: _("No installed package matches all of the currently applied filters"); + icon-name: "funnel-symbolic"; + Button reset_filters_button { + label: _("Reset Filters"); + halign: center; + visible: false; + styles ["pill"] + } + } + Adw.StatusPage no_packages { + title: _("No Packages Found"); + description: _("Warehouse cannot see the list of installed packages or your system has no packages installed"); + icon-name: "error-symbolic"; + } + Adw.StatusPage no_results { + title: _("No Results Found"); + description: _("Try a different search"); + icon-name: "system-search-symbolic"; + } + } + [bottom] + Revealer { + reveal-child: bind select_button.active; + transition-type: slide_up; + [center] + Box bottom_bar { + styles ["toolbar"] + hexpand: true; + homogeneous: true; + Button select_all_button { + styles ["raised"] + Adw.ButtonContent { + icon-name: "selection-mode-symbolic"; + label: _("Select All"); + can-shrink: true; + } + } + MenuButton copy_button { + styles ["raised"] + Adw.ButtonContent { + icon-name: "edit-copy-symbolic"; + label: _("Copy"); + can-shrink: true; + } + popover: copy_pop; + } + Button uninstall_button { + styles ["raised"] + Adw.ButtonContent { + icon-name: "user-trash-symbolic"; + label: _("Uninstall"); + can-shrink: true; + } + } + } + } + } + } + } + ; + content: + Adw.NavigationPage { + title: "Content Stack"; + Stack content_stack { + transition-type: slide_left_right; + $PropertiesPage properties_page {} + $FiltersPage filters_page {} + } + } + ; + } + } + } } Popover copy_pop { - styles ["menu"] - ListBox copy_menu { - Label copy_names { - label: _("Copy Names"); - halign: start; - } - Label copy_ids { - label: _("Copy IDs"); - halign: start; - } - Label copy_refs { - label: _("Copy Refs"); - halign: start; - } - } + styles ["menu"] + ListBox copy_menu { + Label copy_names { + label: _("Copy Names"); + halign: start; + } + Label copy_ids { + label: _("Copy IDs"); + halign: start; + } + Label copy_refs { + label: _("Copy Refs"); + halign: start; + } + } } diff --git a/src/packages_page/packages_page.py b/src/packages_page/packages_page.py index 0824b8b..07b2416 100644 --- a/src/packages_page/packages_page.py +++ b/src/packages_page/packages_page.py @@ -13,388 +13,388 @@ import subprocess, os @Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/packages_page.ui") class PackagesPage(Adw.BreakpointBin): - __gtype_name__ = 'PackagesPage' - gtc = Gtk.Template.Child - packages_bpt = gtc() - packages_toast_overlay = gtc() - stack = gtc() - status_stack = gtc() - scrolled_window = gtc() - loading_view = gtc() - uninstalling_view = gtc() - reinstalling_view = gtc() - changing_version_view = gtc() - no_filter_results = gtc() - reset_filters_button = gtc() - no_packages = gtc() - no_results = gtc() - filter_button = gtc() - search_button = gtc() - search_bar = gtc() - search_entry = gtc() - packages_split = gtc() - packages_list_box = gtc() - select_button = gtc() - packages_navpage = gtc() - select_all_button = gtc() - content_stack = gtc() - copy_button = gtc() - copy_pop = gtc() - copy_menu = gtc() - copy_names = gtc() - copy_ids = gtc() - copy_refs = gtc() - 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: - self.stack.set_visible_child(self.uninstalling_view) - elif to_set is self.reinstalling: - self.stack.set_visible_child(self.reinstalling_view) - elif to_set is self.changing_version: - self.stack.set_visible_child(self.changing_version_view) - 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") - show_runtimes = self.filter_settings.get_boolean("show-runtimes") - remotes_list = self.filter_settings.get_string("remotes-list") - runtimes_list = self.filter_settings.get_string("runtimes-list") - total_visible = 0 - while row := self.packages_list_box.get_row_at_index(i): - i += 1 - visible = True - if row.package.is_runtime and not show_runtimes: - visible = False - if (not row.package.is_runtime) and (not show_apps): - visible = False - if remotes_list != "all" and not f"{row.package.info['origin']}<>{row.package.info['installation']}" in remotes_list: - 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 - while row := self.packages_list_box.get_row_at_index(i): - i += 1 - if row.get_visible(): - 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) - self.uninstall_button.set_sensitive(True) - else: - 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() - self.selected_rows.clear() - GLib.idle_add(lambda *_: self.filters_page.generate_filters()) - self.copy_button.set_sensitive(False) - self.uninstall_button.set_sensitive(False) - 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 - row.masked_status_icon.set_visible(package.is_masked) - row.pinned_status_icon.set_visible(package.is_pinned) - row.eol_package_package_status_icon.set_visible(package.is_eol) - row.check_button.set_visible(self.select_button.get_active()) - row.check_button.connect("toggled", lambda *_, row=row: self.row_select_handler(row)) - try: - if not package.is_runtime: - 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() - subtitle = row.get_subtitle().lower() - 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 = "" - feedback = "" - match row.get_child(): - case self.copy_names: - info = "name" - feedback = _("Names") - case self.copy_ids: - info = "id" - feedback = _("IDs") - case self.copy_refs: - info = "ref" - feedback = _("Refs") - - to_copy = [] - for row in self.selected_rows: - to_copy.append(row.package.info[info]) - to_copy += ['\n'] - try: - HostInfo.clipboard.set("".join(to_copy[:-1])) - 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 = [] - def thread(*args): - HostInfo.main_window.add_refresh_lockout("batch uninstalling packages") - cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y'] - to_uninstall = {} # { <>: [, , , ...], ... } - 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") - if len(error) > 0: - details = "\n\n".join(error) - 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) - self.packages_split.set_show_content(True) - 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")) - self.uninstalling = LoadingStatus(_("Uninstalling Packages"), _("This should only take a moment")) - self.uninstalling_view.set_content(self.uninstalling) - self.reinstalling = LoadingStatus(_("Reinstalling Package"), _("This could take a while"), True, PackageInstallWorker.cancel) - self.reinstalling_view.set_content(self.reinstalling) - self.changing_version = LoadingStatus(_("Changing Version"), _("This could take a while"), True, ChangeVersionWorker.cancel) - self.changing_version_view.set_content(self.changing_version) - self.filter_settings = Gio.Settings.new("io.github.flattool.Warehouse.filter") - self.is_result = False - self.prev_status = None - 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) - self.packages_list_box.set_sort_func(self.sort_func) - 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) - self.packages_list_box.connect("row-activated", self.row_activate_handler) - self.select_button.connect("toggled", self.select_button_handler) - self.filter_button.connect("toggled", self.filter_button_handler) - self.reset_filters_button.connect("clicked", lambda *_: self.filters_page.reset_filters()) - self.packages_split.connect("notify::show-content", self.filter_page_handler) - self.packages_bpt.connect("apply", self.filter_page_handler) - self.select_all_button.connect("clicked", self.select_all_handler) - self.copy_menu.connect("row-activated", self.selection_copy) - self.uninstall_button.connect("clicked", self.selection_uninstall) + __gtype_name__ = 'PackagesPage' + gtc = Gtk.Template.Child + packages_bpt = gtc() + packages_toast_overlay = gtc() + stack = gtc() + status_stack = gtc() + scrolled_window = gtc() + loading_view = gtc() + uninstalling_view = gtc() + reinstalling_view = gtc() + changing_version_view = gtc() + no_filter_results = gtc() + reset_filters_button = gtc() + no_packages = gtc() + no_results = gtc() + filter_button = gtc() + search_button = gtc() + search_bar = gtc() + search_entry = gtc() + packages_split = gtc() + packages_list_box = gtc() + select_button = gtc() + packages_navpage = gtc() + select_all_button = gtc() + content_stack = gtc() + copy_button = gtc() + copy_pop = gtc() + copy_menu = gtc() + copy_names = gtc() + copy_ids = gtc() + copy_refs = gtc() + 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: + self.stack.set_visible_child(self.uninstalling_view) + elif to_set is self.reinstalling: + self.stack.set_visible_child(self.reinstalling_view) + elif to_set is self.changing_version: + self.stack.set_visible_child(self.changing_version_view) + 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") + show_runtimes = self.filter_settings.get_boolean("show-runtimes") + remotes_list = self.filter_settings.get_string("remotes-list") + runtimes_list = self.filter_settings.get_string("runtimes-list") + total_visible = 0 + while row := self.packages_list_box.get_row_at_index(i): + i += 1 + visible = True + if row.package.is_runtime and not show_runtimes: + visible = False + if (not row.package.is_runtime) and (not show_apps): + visible = False + if remotes_list != "all" and not f"{row.package.info['origin']}<>{row.package.info['installation']}" in remotes_list: + 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 + while row := self.packages_list_box.get_row_at_index(i): + i += 1 + if row.get_visible(): + 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) + self.uninstall_button.set_sensitive(True) + else: + 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() + self.selected_rows.clear() + GLib.idle_add(lambda *_: self.filters_page.generate_filters()) + self.copy_button.set_sensitive(False) + self.uninstall_button.set_sensitive(False) + 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 + row.masked_status_icon.set_visible(package.is_masked) + row.pinned_status_icon.set_visible(package.is_pinned) + row.eol_package_package_status_icon.set_visible(package.is_eol) + row.check_button.set_visible(self.select_button.get_active()) + row.check_button.connect("toggled", lambda *_, row=row: self.row_select_handler(row)) + try: + if not package.is_runtime: + 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() + subtitle = row.get_subtitle().lower() + 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 = "" + feedback = "" + match row.get_child(): + case self.copy_names: + info = "name" + feedback = _("Names") + case self.copy_ids: + info = "id" + feedback = _("IDs") + case self.copy_refs: + info = "ref" + feedback = _("Refs") + + to_copy = [] + for row in self.selected_rows: + to_copy.append(row.package.info[info]) + to_copy += ['\n'] + try: + HostInfo.clipboard.set("".join(to_copy[:-1])) + 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 = [] + def thread(*args): + HostInfo.main_window.add_refresh_lockout("batch uninstalling packages") + cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y'] + to_uninstall = {} # { <>: [, , , ...], ... } + 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") + if len(error) > 0: + details = "\n\n".join(error) + 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) + self.packages_split.set_show_content(True) + 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")) + self.uninstalling = LoadingStatus(_("Uninstalling Packages"), _("This should only take a moment")) + self.uninstalling_view.set_content(self.uninstalling) + self.reinstalling = LoadingStatus(_("Reinstalling Package"), _("This could take a while"), True, PackageInstallWorker.cancel) + self.reinstalling_view.set_content(self.reinstalling) + self.changing_version = LoadingStatus(_("Changing Version"), _("This could take a while"), True, ChangeVersionWorker.cancel) + self.changing_version_view.set_content(self.changing_version) + self.filter_settings = Gio.Settings.new("io.github.flattool.Warehouse.filter") + self.is_result = False + self.prev_status = None + 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) + self.packages_list_box.set_sort_func(self.sort_func) + 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) + self.packages_list_box.connect("row-activated", self.row_activate_handler) + self.select_button.connect("toggled", self.select_button_handler) + self.filter_button.connect("toggled", self.filter_button_handler) + self.reset_filters_button.connect("clicked", lambda *_: self.filters_page.reset_filters()) + self.packages_split.connect("notify::show-content", self.filter_page_handler) + self.packages_bpt.connect("apply", self.filter_page_handler) + self.select_all_button.connect("clicked", self.select_all_handler) + self.copy_menu.connect("row-activated", self.selection_copy) + self.uninstall_button.connect("clicked", self.selection_uninstall) diff --git a/src/packages_page/uninstall_dialog.blp b/src/packages_page/uninstall_dialog.blp index 96d2ada..cfd0b1d 100644 --- a/src/packages_page/uninstall_dialog.blp +++ b/src/packages_page/uninstall_dialog.blp @@ -2,26 +2,26 @@ using Gtk 4.0; using Adw 1; template $UninstallDialog : Adw.AlertDialog { - extra-child: - Adw.PreferencesGroup group { - Adw.ActionRow { - title: _("Keep"); - subtitle: _("Allows restoring app settings and content"); - activatable-widget: keep; - [prefix] - CheckButton keep { - active: true; - } - } - Adw.ActionRow { - title: _("Trash"); - subtitle: _("Send data to the trash"); - activatable-widget: trash; - [prefix] - CheckButton trash { - group: keep; - } - } - } - ; + extra-child: + Adw.PreferencesGroup group { + Adw.ActionRow { + title: _("Keep"); + subtitle: _("Allows restoring app settings and content"); + activatable-widget: keep; + [prefix] + CheckButton keep { + active: true; + } + } + Adw.ActionRow { + title: _("Trash"); + subtitle: _("Send data to the trash"); + activatable-widget: trash; + [prefix] + CheckButton trash { + group: keep; + } + } + } + ; } diff --git a/src/packages_page/uninstall_dialog.py b/src/packages_page/uninstall_dialog.py index 535d35f..0a21623 100644 --- a/src/packages_page/uninstall_dialog.py +++ b/src/packages_page/uninstall_dialog.py @@ -2,41 +2,41 @@ from gi.repository import Adw, Gtk, GLib @Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/uninstall_dialog.ui") 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")) - self.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) - self.connect("response", self.on_response) - self.group.set_title(GLib.markup_escape_text(_("App Settings & Content"))) - self.group.set_visible(show_trash_option) + __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")) + self.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) + self.connect("response", self.on_response) + self.group.set_title(GLib.markup_escape_text(_("App Settings & Content"))) + self.group.set_visible(show_trash_option) diff --git a/src/properties_page/properties_page.blp b/src/properties_page/properties_page.blp index 20ac808..17e6b4b 100644 --- a/src/properties_page/properties_page.blp +++ b/src/properties_page/properties_page.blp @@ -2,245 +2,245 @@ using Gtk 4.0; using Adw 1; template $PropertiesPage : Adw.NavigationPage { - title: "Outer Page"; - Stack stack { - Adw.ToolbarView loading_tbv { - [top] - Adw.HeaderBar { - show-title: false; - } - } - Adw.ToolbarView error_tbv { - [top] - Adw.HeaderBar { - show-title: false; - } - Adw.StatusPage { - title: _("Properties Page Unavailable"); - description: _("Cannot show the properties page at this time"); - icon-name: "error-symbolic"; - } - } - Adw.NavigationView nav_view { - Adw.NavigationPage inner_nav_page { - title: "Inner Page"; - Adw.ToastOverlay toast_overlay { - Adw.ToolbarView { - [top] - Adw.HeaderBar header_bar { - show-title: false; - [end] - MenuButton more_menu_button { - icon-name: "view-more-symbolic"; - popover: more_menu; - } - } - ScrolledWindow scrolled_window { - Adw.Clamp { - Box { - margin-start: 12; - margin-end: 12; - margin-bottom: 12; - orientation: vertical; - halign: fill; - - Image app_icon { - pixel-size: 100; - margin-top: 6; - margin-bottom: 18; - icon-name: "application-x-executable-symbolic"; - styles["icon-dropshadow"] - } + title: "Outer Page"; + Stack stack { + Adw.ToolbarView loading_tbv { + [top] + Adw.HeaderBar { + show-title: false; + } + } + Adw.ToolbarView error_tbv { + [top] + Adw.HeaderBar { + show-title: false; + } + Adw.StatusPage { + title: _("Properties Page Unavailable"); + description: _("Cannot show the properties page at this time"); + icon-name: "error-symbolic"; + } + } + Adw.NavigationView nav_view { + Adw.NavigationPage inner_nav_page { + title: "Inner Page"; + Adw.ToastOverlay toast_overlay { + Adw.ToolbarView { + [top] + Adw.HeaderBar header_bar { + show-title: false; + [end] + MenuButton more_menu_button { + icon-name: "view-more-symbolic"; + popover: more_menu; + } + } + ScrolledWindow scrolled_window { + Adw.Clamp { + Box { + margin-start: 12; + margin-end: 12; + margin-bottom: 12; + orientation: vertical; + halign: fill; + + Image app_icon { + pixel-size: 100; + margin-top: 6; + margin-bottom: 18; + icon-name: "application-x-executable-symbolic"; + styles["icon-dropshadow"] + } - Label name { - styles ["title-1"] - wrap: true; - wrap-mode: word_char; - justify: center; - margin-bottom: 12; - margin-start: 6; - margin-end: 6; - } + Label name { + styles ["title-1"] + wrap: true; + wrap-mode: word_char; + justify: center; + margin-bottom: 12; + margin-start: 6; + margin-end: 6; + } - Label description { - styles ["title-4"] - wrap: true; - wrap-mode: word_char; - justify: center; - margin-start: 6; - margin-end: 6; - } + Label description { + styles ["title-4"] + wrap: true; + wrap-mode: word_char; + justify: center; + margin-start: 6; + margin-end: 6; + } - Box { - spacing: 6; - homogeneous: true; - margin-top: 18; - margin-bottom: 12; - halign: center; - Button open_app_button { - styles ["suggested-action", "pill"] - can-shrink: true; - label: _("Open"); - } - Button uninstall_button { - styles ["pill"] - can-shrink: true; - label: _("Uninstall"); - } - } + Box { + spacing: 6; + homogeneous: true; + margin-top: 18; + margin-bottom: 12; + halign: center; + Button open_app_button { + styles ["suggested-action", "pill"] + can-shrink: true; + label: _("Open"); + } + Button uninstall_button { + styles ["pill"] + can-shrink: true; + label: _("Uninstall"); + } + } - Box eol_box { - margin-bottom: 12; - styles ["card"] - Label status_label { - margin-top: 6; - margin-bottom: 7; - margin-start: 6; - margin-end: 6; - label: _("This package is End Of Life, and will not receive any security updates"); - styles ["heading", "error"] - halign: center; - hexpand: true; - wrap: true; - justify: center; - } - } + Box eol_box { + margin-bottom: 12; + styles ["card"] + Label status_label { + margin-top: 6; + margin-bottom: 7; + margin-start: 6; + margin-end: 6; + label: _("This package is End Of Life, and will not receive any security updates"); + styles ["heading", "error"] + halign: center; + hexpand: true; + wrap: true; + justify: center; + } + } - Box information { - orientation: vertical; - Adw.PreferencesGroup actions { - margin-bottom: 12; - Adw.ActionRow data_row { - title: _("User Data"); - styles["property"] + Box information { + orientation: vertical; + Adw.PreferencesGroup actions { + margin-bottom: 12; + Adw.ActionRow data_row { + title: _("User Data"); + styles["property"] - [suffix] - Button open_data_button { - styles["flat"] - valign: center; - icon-name: "folder-open-symbolic"; - tooltip-text: _("Open User Data"); - } + [suffix] + Button open_data_button { + styles["flat"] + valign: center; + icon-name: "folder-open-symbolic"; + tooltip-text: _("Open User Data"); + } - [suffix] - Button trash_data_button { - styles["flat"] - valign: center; - icon-name: "user-trash-symbolic"; - tooltip-text: _("Trash User Data"); - } + [suffix] + Button trash_data_button { + styles["flat"] + valign: center; + icon-name: "user-trash-symbolic"; + tooltip-text: _("Trash User Data"); + } - [suffix] - Spinner data_spinner { - spinning: true; - } - } - Adw.ExpanderRow version_row { - title: _("Version"); - styles ["property"] - [suffix] - Label mask_label { - label: _("Updates Disabled"); - styles["warning"] - } - Adw.ActionRow mask_row { - title: _("Disable Updates"); - subtitle: _("Mask this package so it's never updated"); - activatable: true; - Gtk.Switch mask_switch { - valign: center; - can-focus: false; - can-target: false; - } - } - Adw.ActionRow change_version_row { - title: _("Change Version"); - subtitle: _("Upgrade or downgrade this package"); - activatable: true; - Image { - icon-name: "right-large-symbolic"; - } - } - } - Adw.ActionRow installed_size_row { - styles ["property"] - title: _("Installed Size"); - activatable: true; - Image { - icon-name: "copy-symbolic"; - } - } - Adw.ActionRow runtime_row { - styles ["property"] - title: _("Runtime"); - activatable: true; - Image eol_package_package_status_icon { - icon-name: "error-symbolic"; - tooltip-text: _("This package is End Of Life, and will not recieve any security updates"); - margin-end: 6; - styles["error"] - } - Image { - icon-name: "right-large-symbolic"; - } - } - Adw.ActionRow pin_row { - title: _("Disable Automactic Removal"); - subtitle: _("Pin this runtime to keep it installed"); - activatable: true; - Gtk.Switch pin_switch { - valign: center; - can-focus: false; - can-target: false; - } - } - } - Adw.PreferencesGroup package_info { - margin-bottom: 12; - title: _("Package Information"); - Adw.ActionRow id_row { - styles ["property"] - title: _("Application ID"); - activatable: true; - Image { - icon-name: "copy-symbolic"; - } - } - Adw.ActionRow ref_row { - styles ["property"] - title: "Ref"; - activatable: true; - Image { - icon-name: "copy-symbolic"; - } - } - Adw.ActionRow arch_row { - styles ["property"] - title: _("Architecture"); - activatable: true; - Image { - icon-name: "copy-symbolic"; - } - } - Adw.ActionRow branch_row { - styles ["property"] - title: _("Branch"); - activatable: true; - Image { - icon-name: "copy-symbolic"; - } - } - Adw.ActionRow license_row { - styles ["property"] - title: _("License"); - activatable: true; - Image { - icon-name: "copy-symbolic"; - } - } - } - Adw.PreferencesGroup remote_info { - margin-bottom: 12; + [suffix] + Spinner data_spinner { + spinning: true; + } + } + Adw.ExpanderRow version_row { + title: _("Version"); + styles ["property"] + [suffix] + Label mask_label { + label: _("Updates Disabled"); + styles["warning"] + } + Adw.ActionRow mask_row { + title: _("Disable Updates"); + subtitle: _("Mask this package so it's never updated"); + activatable: true; + Gtk.Switch mask_switch { + valign: center; + can-focus: false; + can-target: false; + } + } + Adw.ActionRow change_version_row { + title: _("Change Version"); + subtitle: _("Upgrade or downgrade this package"); + activatable: true; + Image { + icon-name: "right-large-symbolic"; + } + } + } + Adw.ActionRow installed_size_row { + styles ["property"] + title: _("Installed Size"); + activatable: true; + Image { + icon-name: "copy-symbolic"; + } + } + Adw.ActionRow runtime_row { + styles ["property"] + title: _("Runtime"); + activatable: true; + Image eol_package_package_status_icon { + icon-name: "error-symbolic"; + tooltip-text: _("This package is End Of Life, and will not recieve any security updates"); + margin-end: 6; + styles["error"] + } + Image { + icon-name: "right-large-symbolic"; + } + } + Adw.ActionRow pin_row { + title: _("Disable Automactic Removal"); + subtitle: _("Pin this runtime to keep it installed"); + activatable: true; + Gtk.Switch pin_switch { + valign: center; + can-focus: false; + can-target: false; + } + } + } + Adw.PreferencesGroup package_info { + margin-bottom: 12; + title: _("Package Information"); + Adw.ActionRow id_row { + styles ["property"] + title: _("Application ID"); + activatable: true; + Image { + icon-name: "copy-symbolic"; + } + } + Adw.ActionRow ref_row { + styles ["property"] + title: "Ref"; + activatable: true; + Image { + icon-name: "copy-symbolic"; + } + } + Adw.ActionRow arch_row { + styles ["property"] + title: _("Architecture"); + activatable: true; + Image { + icon-name: "copy-symbolic"; + } + } + Adw.ActionRow branch_row { + styles ["property"] + title: _("Branch"); + activatable: true; + Image { + icon-name: "copy-symbolic"; + } + } + Adw.ActionRow license_row { + styles ["property"] + title: _("License"); + activatable: true; + Image { + icon-name: "copy-symbolic"; + } + } + } + Adw.PreferencesGroup remote_info { + margin-bottom: 12; title: _("Installation Information"); Adw.ActionRow sdk_row { styles ["property"] diff --git a/src/properties_page/properties_page.py b/src/properties_page/properties_page.py index c8ae9f4..89ad20e 100644 --- a/src/properties_page/properties_page.py +++ b/src/properties_page/properties_page.py @@ -9,382 +9,382 @@ import subprocess, os @Gtk.Template(resource_path="/io/github/flattool/Warehouse/properties_page/properties_page.ui") class PropertiesPage(Adw.NavigationPage): - __gtype_name__ = 'PropertiesPage' - gtc = Gtk.Template.Child - stack = gtc() - error_tbv = gtc() - loading_tbv = gtc() - - more_menu = gtc() - more_list = gtc() - - nav_view = gtc() - inner_nav_page = gtc() - toast_overlay = gtc() - header_bar = gtc() - scrolled_window = gtc() - app_icon = gtc() - name = gtc() - description = gtc() - eol_box = gtc() - open_app_button = gtc() - uninstall_button = gtc() - - pin_row = gtc() - pin_switch = gtc() - data_row = gtc() - open_data_button = gtc() - trash_data_button = gtc() - data_spinner = gtc() - version_row = gtc() - mask_label = gtc() - mask_row = gtc() - mask_switch = gtc() - change_version_row = gtc() - 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"] - if pkg_name != "": - self.inner_nav_page.set_title(_("{} Properties").format(package.info["name"])) - self.name.set_visible(True) - self.name.set_label(pkg_name) - 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) - self.open_app_button.set_visible(not package.is_runtime) - self.data_row.set_visible(not package.is_runtime) - self.uninstall_button.set_sensitive(self.package.info['id'] != "io.github.flattool.Warehouse") - if package.is_runtime: - self.runtime_row.set_visible(False) - else: - 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() - pkg_description = package.cli_info["description"] - self.description.set_visible(pkg_description != "") - self.description.set_label(pkg_description) - 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) - row.set_visible(True) - except KeyError: - if key == "version": - row.set_visible(True) - row.set_subtitle(_("No version information found")) - continue - 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) - GLib.idle_add(lambda *_: self.stack.set_visible_child(self.nav_view)) - self.more_list.remove_all() - 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': - return - try: - self.package.trash_data() - self.set_properties(self.package, refresh=True) - self.toast_overlay.add_toast(Adw.Toast.new("Trashed User Data")) - user_data_page = HostInfo.main_window.pages[HostInfo.main_window.user_data_row] - user_data_page.start_loading() - user_data_page.end_loading() - 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.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") - ) - dialog.add_response('cancel', _("Cancel")) - dialog.add_response('continue', _("Trash Data")) - 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): - if fail := self.package.failed_mask: - response = _("Could not Disable Updates") if state else _("Could not Enable Updates") - fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail - self.toast_overlay.add_toast(ErrorToast(response, str(fail)).toast) - GLib.idle_add(lambda *_: self.mask_switch.set_active(not state)) - GLib.idle_add(lambda *_: self.mask_label.set_visible(not state)) - else: - response = _("Disabled Updates") if state else _("Enabled Updates") - self.toast_overlay.add_toast(Adw.Toast(title=response)) - 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): - if fail := self.package.failed_pin: - response = _("Could not Disable Autoremoval") if state else _("Could not Enable Autoremoval") - fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail - self.toast_overlay.add_toast(ErrorToast(response, str(fail)).toast) - GLib.idle_add(lambda *_: self.pin_switch.set_active(not state)) - else: - response = _("Disabled Autoremoval") if state else _("Enabled Autoremoval") - 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) - self.package.uninstall(callback) - if should_trash: - try: - self.package.trash_data() - self.set_properties(self.package, refresh=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) - - def callback(*args): - if fail := self.package.failed_uninstall: - fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail - self.toast_overlay.add_toast(ErrorToast(_("Could not uninstall"), str(fail)).toast) - self.packages_page.set_status(self.packages_page.scrolled_window) - 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) == 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( - [{ - "installation": self.package.info['installation'], - "remote": self.package.info['origin'], - "package_names": [self.package.info['ref']], - "extra_flags": ["--reinstall"], - }], - self.packages_page.reinstalling, - self.reinstall_callback, - 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.") - ) - dialog.add_response("cancel", _("Cancel")) - dialog.add_response("continue", _("Reinstall")) - 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(): - case self.view_snapshots: - snapshots_row = HostInfo.main_window.snapshots_row - 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, - "date": self.date_row, - } - self.loading_tbv.set_content(LoadingStatus(_("Loading Properties"), _("This should only take a moment"))) - self.packages_page = None # To be set in packages page - self.__class__.main_window = self.main_window - self.view_snapshots = Gtk.Label(halign=Gtk.Align.START, label=_("View Snapshots")) - self.copy_launch_command = Gtk.Label(halign=Gtk.Align.START, label=_("Copy Launch Command")) - 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) - self.scrolled_window.get_vadjustment().connect("value-changed", lambda adjustment: self.header_bar.set_show_title(not adjustment.get_value() == 0)) - self.trash_data_button.connect("clicked", self.trash_data_handler) - self.runtime_row.connect("activated", self.runtime_row_handler) - self.open_app_button.connect("clicked", self.open_app_handler) - self.uninstall_button.connect("clicked", self.uninstall_handler) - self.mask_row.connect("activated", self.set_mask_handler) - self.pin_row.connect("activated", self.set_pin_handler) - self.change_version_row.connect("activated", self.change_version_handler) - for key, row in self.info_rows.items(): - if type(row) is Adw.ActionRow: - row.connect("activated", self.copy_handler) + __gtype_name__ = 'PropertiesPage' + gtc = Gtk.Template.Child + stack = gtc() + error_tbv = gtc() + loading_tbv = gtc() + + more_menu = gtc() + more_list = gtc() + + nav_view = gtc() + inner_nav_page = gtc() + toast_overlay = gtc() + header_bar = gtc() + scrolled_window = gtc() + app_icon = gtc() + name = gtc() + description = gtc() + eol_box = gtc() + open_app_button = gtc() + uninstall_button = gtc() + + pin_row = gtc() + pin_switch = gtc() + data_row = gtc() + open_data_button = gtc() + trash_data_button = gtc() + data_spinner = gtc() + version_row = gtc() + mask_label = gtc() + mask_row = gtc() + mask_switch = gtc() + change_version_row = gtc() + 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"] + if pkg_name != "": + self.inner_nav_page.set_title(_("{} Properties").format(package.info["name"])) + self.name.set_visible(True) + self.name.set_label(pkg_name) + 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) + self.open_app_button.set_visible(not package.is_runtime) + self.data_row.set_visible(not package.is_runtime) + self.uninstall_button.set_sensitive(self.package.info['id'] != "io.github.flattool.Warehouse") + if package.is_runtime: + self.runtime_row.set_visible(False) + else: + 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() + pkg_description = package.cli_info["description"] + self.description.set_visible(pkg_description != "") + self.description.set_label(pkg_description) + 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) + row.set_visible(True) + except KeyError: + if key == "version": + row.set_visible(True) + row.set_subtitle(_("No version information found")) + continue + 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) + GLib.idle_add(lambda *_: self.stack.set_visible_child(self.nav_view)) + self.more_list.remove_all() + 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': + return + try: + self.package.trash_data() + self.set_properties(self.package, refresh=True) + self.toast_overlay.add_toast(Adw.Toast.new("Trashed User Data")) + user_data_page = HostInfo.main_window.pages[HostInfo.main_window.user_data_row] + user_data_page.start_loading() + user_data_page.end_loading() + 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.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") + ) + dialog.add_response('cancel', _("Cancel")) + dialog.add_response('continue', _("Trash Data")) + 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): + if fail := self.package.failed_mask: + response = _("Could not Disable Updates") if state else _("Could not Enable Updates") + fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail + self.toast_overlay.add_toast(ErrorToast(response, str(fail)).toast) + GLib.idle_add(lambda *_: self.mask_switch.set_active(not state)) + GLib.idle_add(lambda *_: self.mask_label.set_visible(not state)) + else: + response = _("Disabled Updates") if state else _("Enabled Updates") + self.toast_overlay.add_toast(Adw.Toast(title=response)) + 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): + if fail := self.package.failed_pin: + response = _("Could not Disable Autoremoval") if state else _("Could not Enable Autoremoval") + fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail + self.toast_overlay.add_toast(ErrorToast(response, str(fail)).toast) + GLib.idle_add(lambda *_: self.pin_switch.set_active(not state)) + else: + response = _("Disabled Autoremoval") if state else _("Enabled Autoremoval") + 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) + self.package.uninstall(callback) + if should_trash: + try: + self.package.trash_data() + self.set_properties(self.package, refresh=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) + + def callback(*args): + if fail := self.package.failed_uninstall: + fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail + self.toast_overlay.add_toast(ErrorToast(_("Could not uninstall"), str(fail)).toast) + self.packages_page.set_status(self.packages_page.scrolled_window) + 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) == 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( + [{ + "installation": self.package.info['installation'], + "remote": self.package.info['origin'], + "package_names": [self.package.info['ref']], + "extra_flags": ["--reinstall"], + }], + self.packages_page.reinstalling, + self.reinstall_callback, + 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.") + ) + dialog.add_response("cancel", _("Cancel")) + dialog.add_response("continue", _("Reinstall")) + 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(): + case self.view_snapshots: + snapshots_row = HostInfo.main_window.snapshots_row + 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, + "date": self.date_row, + } + self.loading_tbv.set_content(LoadingStatus(_("Loading Properties"), _("This should only take a moment"))) + self.packages_page = None # To be set in packages page + self.__class__.main_window = self.main_window + self.view_snapshots = Gtk.Label(halign=Gtk.Align.START, label=_("View Snapshots")) + self.copy_launch_command = Gtk.Label(halign=Gtk.Align.START, label=_("Copy Launch Command")) + 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) + self.scrolled_window.get_vadjustment().connect("value-changed", lambda adjustment: self.header_bar.set_show_title(not adjustment.get_value() == 0)) + self.trash_data_button.connect("clicked", self.trash_data_handler) + self.runtime_row.connect("activated", self.runtime_row_handler) + self.open_app_button.connect("clicked", self.open_app_handler) + self.uninstall_button.connect("clicked", self.uninstall_handler) + self.mask_row.connect("activated", self.set_mask_handler) + self.pin_row.connect("activated", self.set_pin_handler) + self.change_version_row.connect("activated", self.change_version_handler) + for key, row in self.info_rows.items(): + if type(row) is Adw.ActionRow: + row.connect("activated", self.copy_handler) diff --git a/src/remotes_page/add_remote_dialog.blp b/src/remotes_page/add_remote_dialog.blp index dde66c1..1a53273 100644 --- a/src/remotes_page/add_remote_dialog.blp +++ b/src/remotes_page/add_remote_dialog.blp @@ -2,55 +2,55 @@ using Gtk 4.0; using Adw 1; template $AddRemoteDialog : Adw.Dialog { - title: _("Add a Remote"); - // content-width: 500; - // content-height: 375; - // width-request: 400; - follows-content-size: true; - Adw.ToolbarView { - [top] - Adw.HeaderBar { - show-start-title-buttons: false; - show-end-title-buttons: false; - [start] - Button cancel_button { - label: _("Cancel"); - } - [end] - Button apply_button { - styles ["suggested-action"] - label: _("Add"); - } - } - Adw.ToastOverlay toast_overlay { - ScrolledWindow content_page { - propagate-natural-height: true; - propagate-natural-width: true; - Adw.Clamp { - margin-start: 12; - margin-end: 12; - margin-top: 12; - margin-bottom: 12; - Box { - orientation: vertical; - spacing: 12; - Adw.PreferencesGroup { - styles ["boxed-list"] - Adw.EntryRow title_row { - title: _("Title"); - } - Adw.EntryRow name_row { - title: _("Name"); - } - Adw.EntryRow url_row { - title: _("Repo URL"); - } - } - $InstallationChooser installation_chooser { - } - } - } - } - } - } + title: _("Add a Remote"); + // content-width: 500; + // content-height: 375; + // width-request: 400; + follows-content-size: true; + Adw.ToolbarView { + [top] + Adw.HeaderBar { + show-start-title-buttons: false; + show-end-title-buttons: false; + [start] + Button cancel_button { + label: _("Cancel"); + } + [end] + Button apply_button { + styles ["suggested-action"] + label: _("Add"); + } + } + Adw.ToastOverlay toast_overlay { + ScrolledWindow content_page { + propagate-natural-height: true; + propagate-natural-width: true; + Adw.Clamp { + margin-start: 12; + margin-end: 12; + margin-top: 12; + margin-bottom: 12; + Box { + orientation: vertical; + spacing: 12; + Adw.PreferencesGroup { + styles ["boxed-list"] + Adw.EntryRow title_row { + title: _("Title"); + } + Adw.EntryRow name_row { + title: _("Name"); + } + Adw.EntryRow url_row { + title: _("Repo URL"); + } + } + $InstallationChooser installation_chooser { + } + } + } + } + } + } } diff --git a/src/remotes_page/add_remote_dialog.py b/src/remotes_page/add_remote_dialog.py index 8b31e8c..165be7e 100644 --- a/src/remotes_page/add_remote_dialog.py +++ b/src/remotes_page/add_remote_dialog.py @@ -7,125 +7,125 @@ import subprocess, re @Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/add_remote_dialog.ui") class AddRemoteDialog(Adw.Dialog): - __gtype_name__ = "AddRemoteDialog" - gtc = Gtk.Template.Child - - toast_overlay = gtc() - cancel_button = gtc() - apply_button = gtc() - content_page = gtc() - title_row = gtc() - name_row = gtc() - 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) - error = [None] - def thread(*args): - HostInfo.main_window.add_refresh_lockout("adding remote") - cmd = [ - 'flatpak-spawn', '--host', - 'flatpak', 'remote-add', - f'--title={self.title_row.get_text()}', - self.name_row.get_text(), - self.url_row.get_text(), - ] - installation = self.installation_chooser.get_installation() - if installation == "user" or installation == "system": - 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]: - self.parent_page.status_stack.set_visible_child(self.parent_page.main_view) - self.apply_button.set_sensitive(True) - self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not add remote"), str(error[0])).toast) - 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) - case self.name_row: - 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\-._]+$", - self.url_row: r"^[a-zA-Z0-9\-._~:/?#[\]@!$&\'()*+,;= ]+$", - } - self.title_passes = False - self.name_passes = False - self.url_passes = False - - # Apply - self.installation_chooser.set_content_strings(_("remote"), False) - if remote_info: - self.title_row.set_text(remote_info["title"]) - self.name_row.set_text(remote_info["name"]) - self.url_row.set_text(remote_info["link"]) - if remote_info["description"] == "local file": - self.check_entries(self.title_row) - self.check_entries(self.name_row) - self.check_entries(self.url_row) - self.url_row.set_editable(False) - else: - self.title_row.set_editable(False) - self.name_row.set_editable(False) - self.url_row.set_editable(False) - 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()) - self.apply_button.connect("clicked", self.on_apply) - self.title_row.connect("changed", self.check_entries) - self.name_row.connect("changed", self.check_entries) - self.url_row.connect("changed", self.check_entries) + __gtype_name__ = "AddRemoteDialog" + gtc = Gtk.Template.Child + + toast_overlay = gtc() + cancel_button = gtc() + apply_button = gtc() + content_page = gtc() + title_row = gtc() + name_row = gtc() + 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) + error = [None] + def thread(*args): + HostInfo.main_window.add_refresh_lockout("adding remote") + cmd = [ + 'flatpak-spawn', '--host', + 'flatpak', 'remote-add', + f'--title={self.title_row.get_text()}', + self.name_row.get_text(), + self.url_row.get_text(), + ] + installation = self.installation_chooser.get_installation() + if installation == "user" or installation == "system": + 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]: + self.parent_page.status_stack.set_visible_child(self.parent_page.main_view) + self.apply_button.set_sensitive(True) + self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not add remote"), str(error[0])).toast) + 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) + case self.name_row: + 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\-._]+$", + self.url_row: r"^[a-zA-Z0-9\-._~:/?#[\]@!$&\'()*+,;= ]+$", + } + self.title_passes = False + self.name_passes = False + self.url_passes = False + + # Apply + self.installation_chooser.set_content_strings(_("remote"), False) + if remote_info: + self.title_row.set_text(remote_info["title"]) + self.name_row.set_text(remote_info["name"]) + self.url_row.set_text(remote_info["link"]) + if remote_info["description"] == "local file": + self.check_entries(self.title_row) + self.check_entries(self.name_row) + self.check_entries(self.url_row) + self.url_row.set_editable(False) + else: + self.title_row.set_editable(False) + self.name_row.set_editable(False) + self.url_row.set_editable(False) + 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()) + self.apply_button.connect("clicked", self.on_apply) + self.title_row.connect("changed", self.check_entries) + self.name_row.connect("changed", self.check_entries) + self.url_row.connect("changed", self.check_entries) diff --git a/src/remotes_page/remote_row.blp b/src/remotes_page/remote_row.blp index c32c455..3175ed0 100644 --- a/src/remotes_page/remote_row.blp +++ b/src/remotes_page/remote_row.blp @@ -2,56 +2,56 @@ using Gtk 4.0; using Adw 1; template $RemoteRow : Adw.ActionRow { - [suffix] - Label suffix_label { - styles ["subtitle"] - margin-end: 6; - wrap: true; - wrap-mode: word_char; - natural-wrap-mode: none; - halign: end; - hexpand: true; - justify: right; - } - [suffix] - Button filter_button { - styles ["flat"] - valign: center; - icon-name: "funnel-symbolic"; - tooltip-text: _("Set a Filter for this Remote"); - } - [suffix] - MenuButton menu_button { - styles ["flat"] - valign: center; - popover: menu_pop; - icon-name: "view-more-symbolic"; - tooltip-text: _("More Actions"); - } + [suffix] + Label suffix_label { + styles ["subtitle"] + margin-end: 6; + wrap: true; + wrap-mode: word_char; + natural-wrap-mode: none; + halign: end; + hexpand: true; + justify: right; + } + [suffix] + Button filter_button { + styles ["flat"] + valign: center; + icon-name: "funnel-symbolic"; + tooltip-text: _("Set a Filter for this Remote"); + } + [suffix] + MenuButton menu_button { + styles ["flat"] + valign: center; + popover: menu_pop; + icon-name: "view-more-symbolic"; + tooltip-text: _("More Actions"); + } } Popover menu_pop { - styles ["menu"] - ListBox menu_listbox { - Label copy_title { - label: _("Copy Title"); - halign: start; - } - Label copy_name { - label: _("Copy Name"); - halign: start; - } - Label enable_remote { - label: _("Enable"); - halign: start; - } - Label disable_remote { - label: _("Disable"); - halign: start; - } - Label remove { - label: _("Remove"); - halign: start; - } - } + styles ["menu"] + ListBox menu_listbox { + Label copy_title { + label: _("Copy Title"); + halign: start; + } + Label copy_name { + label: _("Copy Name"); + halign: start; + } + Label enable_remote { + label: _("Enable"); + halign: start; + } + Label disable_remote { + label: _("Disable"); + halign: start; + } + Label remove { + label: _("Remove"); + halign: start; + } + } } diff --git a/src/remotes_page/remote_row.py b/src/remotes_page/remote_row.py index 0a0b2dd..2f26352 100644 --- a/src/remotes_page/remote_row.py +++ b/src/remotes_page/remote_row.py @@ -5,168 +5,168 @@ import subprocess @Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remote_row.ui") class RemoteRow(Adw.ActionRow): - __gtype_name__ = 'RemoteRow' - gtc = Gtk.Template.Child + __gtype_name__ = 'RemoteRow' + gtc = Gtk.Template.Child - suffix_label = gtc() - filter_button = gtc() - menu_pop = gtc() - menu_listbox = gtc() + suffix_label = gtc() + filter_button = gtc() + menu_pop = gtc() + menu_listbox = gtc() - copy_title = gtc() - copy_name = gtc() - enable_remote = gtc() - disable_remote = gtc() - remove = gtc() + copy_title = gtc() + copy_name = gtc() + enable_remote = gtc() + disable_remote = gtc() + remove = gtc() - def enable_remote_handler(self, *args): - if not self.remote.disabled: - self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), _("Remote is already enabled")).toast) - return + def enable_remote_handler(self, *args): + if not self.remote.disabled: + self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), _("Remote is already enabled")).toast) + return - has_error = [] - def thread(*args): - cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--enable', self.remote.name] - if self.installation == "user" or self.installation == "system": - cmd.append(f"--{self.installation}") - else: - cmd.append(f"--installation={self.installation}") + has_error = [] + def thread(*args): + cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--enable', self.remote.name] + if self.installation == "user" or self.installation == "system": + cmd.append(f"--{self.installation}") + else: + cmd.append(f"--installation={self.installation}") - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as cpe: - has_error.append(str(cpe.stderr)) - except Exception as e: - has_error.append(str(e)) + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as cpe: + has_error.append(str(cpe.stderr)) + except Exception as e: + has_error.append(str(e)) - def callback(*args): - 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("") - self.remote.disabled = False - self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Enabled remote"))) - self.menu_listbox.get_row_at_index(2).set_visible(False) - self.menu_listbox.get_row_at_index(3).set_visible(True) - self.parent_page.total_disabled -= 1 - install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] - install_page.start_loading() - install_page.end_loading() - filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page - filters_page.generate_filters() - 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 callback(*args): + 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("") + self.remote.disabled = False + self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Enabled remote"))) + self.menu_listbox.get_row_at_index(2).set_visible(False) + self.menu_listbox.get_row_at_index(3).set_visible(True) + self.parent_page.total_disabled -= 1 + install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] + install_page.start_loading() + install_page.end_loading() + filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page + filters_page.generate_filters() + 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): - error = [None] + def disable_remote_handler(self, *args): + error = [None] - def callback(*args): - if error[0]: - return + def callback(*args): + if error[0]: + return - self.add_css_class("warning") - self.set_icon_name("error-symbolic") - self.set_tooltip_text(_("Remote is Disabled")) - self.remote.disabled = True - self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Disabled remote"))) - self.menu_listbox.get_row_at_index(2).set_visible(True) - self.menu_listbox.get_row_at_index(3).set_visible(False) - self.set_visible(self.parent_page.show_disabled_button.get_active()) - self.parent_page.show_disabled_button.set_visible(True) - self.parent_page.total_disabled += 1 - self.parent_page.none_visible_handler() - install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] - install_page.start_loading() - install_page.end_loading() - filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page - filters_page.settings.reset("remotes-list") - filters_page.all_remotes_switch.set_active(False) - filters_page.generate_filters() - filters_page.packages_page.apply_filters() + self.add_css_class("warning") + self.set_icon_name("error-symbolic") + self.set_tooltip_text(_("Remote is Disabled")) + self.remote.disabled = True + self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Disabled remote"))) + self.menu_listbox.get_row_at_index(2).set_visible(True) + self.menu_listbox.get_row_at_index(3).set_visible(False) + self.set_visible(self.parent_page.show_disabled_button.get_active()) + self.parent_page.show_disabled_button.set_visible(True) + self.parent_page.total_disabled += 1 + self.parent_page.none_visible_handler() + install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] + install_page.start_loading() + install_page.end_loading() + filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page + filters_page.settings.reset("remotes-list") + filters_page.all_remotes_switch.set_active(False) + filters_page.generate_filters() + filters_page.packages_page.apply_filters() - def thread(*args): - if self.remote.disabled: - self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), _("Remote is already disabled")).toast) - return + def thread(*args): + if self.remote.disabled: + self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), _("Remote is already disabled")).toast) + return - cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--disable', self.remote.name] - if self.installation == "user" or self.installation == "system": - cmd.append(f"--{self.installation}") - else: - cmd.append(f"--installation={self.installation}") + cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--disable', self.remote.name] + if self.installation == "user" or self.installation == "system": + cmd.append(f"--{self.installation}") + else: + cmd.append(f"--installation={self.installation}") - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as cpe: - GLib.idle_add(lambda *args, cpe=cpe: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), str(cpe.stderr)).toast)) - error[0] = cpe - return - except Exception as e: - GLib.idle_add(lambda *args, e=e: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), str(e)).toast)) - error[0] = e - return + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as cpe: + GLib.idle_add(lambda *args, cpe=cpe: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), str(cpe.stderr)).toast)) + error[0] = cpe + return + except Exception as e: + GLib.idle_add(lambda *args, e=e: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), str(e)).toast)) + error[0] = e + return - def on_response(_, response): - if response != "continue": - return - - Gio.Task.new(None, None, callback).run_in_thread(thread) + 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)) - dialog.add_response("cancel", _("Cancel")) - dialog.add_response("continue", _("Disable")) - dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) - dialog.connect("response", on_response) - dialog.present(self.parent_page.main_window) + dialog = Adw.AlertDialog(heading=_("Disable {}?").format(self.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(self.remote.name)) + dialog.add_response("cancel", _("Cancel")) + dialog.add_response("continue", _("Disable")) + dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) + dialog.connect("response", on_response) + dialog.present(self.parent_page.main_window) - def on_menu_action(self, listbox, row): - row = row.get_child() - match row: - case self.copy_title: - HostInfo.clipboard.set(self.remote.title) - self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied title"))) - case self.copy_name: - HostInfo.clipboard.set(self.remote.name) - self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied name"))) - case self.enable_remote: - self.enable_remote_handler() - case self.disable_remote: - self.disable_remote_handler() - case self.remove: - self.parent_page.remove_remote(self) + def on_menu_action(self, listbox, row): + row = row.get_child() + match row: + case self.copy_title: + HostInfo.clipboard.set(self.remote.title) + self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied title"))) + case self.copy_name: + HostInfo.clipboard.set(self.remote.name) + self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied name"))) + case self.enable_remote: + self.enable_remote_handler() + case self.disable_remote: + self.disable_remote_handler() + case self.remove: + self.parent_page.remove_remote(self) - self.menu_pop.popdown() + self.menu_pop.popdown() - def idle_stuff(self): - self.set_title(self.remote.title) - self.set_subtitle(_("Installation: {}").format(self.installation)) - self.suffix_label.set_label(self.remote.name) - if self.remote.disabled: - self.set_icon_name("error-symbolic") - self.add_css_class("warning") - self.set_tooltip_text(_("Remote is Disabled")) + def idle_stuff(self): + self.set_title(self.remote.title) + self.set_subtitle(_("Installation: {}").format(self.installation)) + self.suffix_label.set_label(self.remote.name) + if self.remote.disabled: + self.set_icon_name("error-symbolic") + self.add_css_class("warning") + self.set_tooltip_text(_("Remote is Disabled")) - def __init__(self, parent_page, installation, remote, **kwargs): - super().__init__(**kwargs) + def __init__(self, parent_page, installation, remote, **kwargs): + super().__init__(**kwargs) - # Extra Object Creation - self.parent_page = parent_page - self.remote = remote - self.installation = installation + # Extra Object Creation + self.parent_page = parent_page + self.remote = remote + self.installation = installation - # Apply - GLib.idle_add(lambda *_: self.idle_stuff()) + # Apply + GLib.idle_add(lambda *_: self.idle_stuff()) - ## Show / Hide the Enable / Disable actions depending on remote status - self.menu_listbox.get_row_at_index(2).set_visible(remote.disabled) - self.menu_listbox.get_row_at_index(3).set_visible(not remote.disabled) + ## Show / Hide the Enable / Disable actions depending on remote status + self.menu_listbox.get_row_at_index(2).set_visible(remote.disabled) + self.menu_listbox.get_row_at_index(3).set_visible(not remote.disabled) - # Connections - self.menu_listbox.connect("row-activated", self.on_menu_action) - self.filter_button.connect("clicked", lambda *_: parent_page.filter_remote(self)) + # Connections + self.menu_listbox.connect("row-activated", self.on_menu_action) + self.filter_button.connect("clicked", lambda *_: parent_page.filter_remote(self)) diff --git a/src/remotes_page/remotes_page.blp b/src/remotes_page/remotes_page.blp index 04afc38..2d04954 100644 --- a/src/remotes_page/remotes_page.blp +++ b/src/remotes_page/remotes_page.blp @@ -2,160 +2,160 @@ using Gtk 4.0; using Adw 1; template $RemotesPage : Adw.NavigationPage { - title: _("Manage Remotes"); - Adw.ToastOverlay toast_overlay { - Stack status_stack { - Adw.ToolbarView loading_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.ToolbarView adding_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.ToolbarView main_view { - [top] - Adw.HeaderBar header_bar { - [start] - $SidebarButton {} - [start] - ToggleButton search_button { - icon-name: "loupe-large-symbolic"; - tooltip-text: _("Search Packages"); - } - } - [top] - Adw.Clamp { - SearchBar search_bar { - search-mode-enabled: bind search_button.active bidirectional; - key-capture-widget: template; - SearchEntry search_entry { - hexpand: true; - placeholder-text: _("Search Remotes"); - } - } - } - Stack stack { - Adw.PreferencesPage content_page { - Adw.PreferencesGroup current_remotes_group { - title: _("Current Remotes"); - description: _("Remotes available on your system"); - header-suffix: - ToggleButton show_disabled_button { - valign: center; - styles ["flat"] - Adw.ButtonContent show_disabled_button_content { - icon-name: "eye-not-looking-symbolic"; - label: _("Show Disabled"); - } - } - ; - Adw.ActionRow none_visible { - styles ["warning"] - [child] - Box { - spacing: 3; - orientation: vertical; - Box { - halign: center; - Image { - valign: center; - margin-top: 7; - margin-end: 6; - icon-name: "eye-not-looking-symbolic"; - } - Label { - margin-top: 7; - label: _("No Enabled Remotes"); - wrap: true; - styles ["heading"] - } - } - Label { - label: _("You only have disabled remotes on this system"); - margin-start: 16; - margin-end: 16; - margin-bottom: 8; - justify: center; - halign: center; - wrap: true; - } - } - } - Adw.ActionRow no_remotes { - styles ["error"] - [child] - Box { - spacing: 3; - orientation: vertical; - Box { - halign: center; - Image { - valign: center; - margin-top: 7; - margin-end: 6; - icon-name: "error-symbolic"; - } - Label { - margin-top: 7; - label: _("No Remotes Found"); - wrap: true; - styles ["heading"] - } - } - Label { - label: _("Warehouse cannot see the current remotes or your system has no remotes added"); - margin-start: 16; - margin-end: 16; - margin-bottom: 8; - justify: center; - halign: center; - wrap: true; - } - } - } - } - Adw.PreferencesGroup new_remotes_group { - visible: bind search_button.active inverted; - title: _("Add Popular Remotes"); - description: _("Add new remotes to get more software"); - } - Adw.PreferencesGroup other_remotes { - visible: bind search_button.active inverted; - title: _("Add Other Remotes"); - Adw.ActionRow file_remote_row { - activatable: true; - title: _("Add a Repo File"); - subtitle: _("Open a downloaded repo file to add"); - [suffix] - Image { - icon-name: "plus-large-symbolic"; - } - } - Adw.ActionRow custom_remote_row { - activatable: true; - title: _("Add a Custom Remote"); - subtitle: _("Manually enter new remote details"); - [suffix] - Image { - icon-name: "plus-large-symbolic"; - } - } - } - } - Adw.StatusPage no_results { - title: _("No Results Found"); - description: _("Try a different search"); - icon-name: "system-search-symbolic"; - } - } - } - } - } + title: _("Manage Remotes"); + Adw.ToastOverlay toast_overlay { + Stack status_stack { + Adw.ToolbarView loading_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.ToolbarView adding_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.ToolbarView main_view { + [top] + Adw.HeaderBar header_bar { + [start] + $SidebarButton {} + [start] + ToggleButton search_button { + icon-name: "loupe-large-symbolic"; + tooltip-text: _("Search Packages"); + } + } + [top] + Adw.Clamp { + SearchBar search_bar { + search-mode-enabled: bind search_button.active bidirectional; + key-capture-widget: template; + SearchEntry search_entry { + hexpand: true; + placeholder-text: _("Search Remotes"); + } + } + } + Stack stack { + Adw.PreferencesPage content_page { + Adw.PreferencesGroup current_remotes_group { + title: _("Current Remotes"); + description: _("Remotes available on your system"); + header-suffix: + ToggleButton show_disabled_button { + valign: center; + styles ["flat"] + Adw.ButtonContent show_disabled_button_content { + icon-name: "eye-not-looking-symbolic"; + label: _("Show Disabled"); + } + } + ; + Adw.ActionRow none_visible { + styles ["warning"] + [child] + Box { + spacing: 3; + orientation: vertical; + Box { + halign: center; + Image { + valign: center; + margin-top: 7; + margin-end: 6; + icon-name: "eye-not-looking-symbolic"; + } + Label { + margin-top: 7; + label: _("No Enabled Remotes"); + wrap: true; + styles ["heading"] + } + } + Label { + label: _("You only have disabled remotes on this system"); + margin-start: 16; + margin-end: 16; + margin-bottom: 8; + justify: center; + halign: center; + wrap: true; + } + } + } + Adw.ActionRow no_remotes { + styles ["error"] + [child] + Box { + spacing: 3; + orientation: vertical; + Box { + halign: center; + Image { + valign: center; + margin-top: 7; + margin-end: 6; + icon-name: "error-symbolic"; + } + Label { + margin-top: 7; + label: _("No Remotes Found"); + wrap: true; + styles ["heading"] + } + } + Label { + label: _("Warehouse cannot see the current remotes or your system has no remotes added"); + margin-start: 16; + margin-end: 16; + margin-bottom: 8; + justify: center; + halign: center; + wrap: true; + } + } + } + } + Adw.PreferencesGroup new_remotes_group { + visible: bind search_button.active inverted; + title: _("Add Popular Remotes"); + description: _("Add new remotes to get more software"); + } + Adw.PreferencesGroup other_remotes { + visible: bind search_button.active inverted; + title: _("Add Other Remotes"); + Adw.ActionRow file_remote_row { + activatable: true; + title: _("Add a Repo File"); + subtitle: _("Open a downloaded repo file to add"); + [suffix] + Image { + icon-name: "plus-large-symbolic"; + } + } + Adw.ActionRow custom_remote_row { + activatable: true; + title: _("Add a Custom Remote"); + subtitle: _("Manually enter new remote details"); + [suffix] + Image { + icon-name: "plus-large-symbolic"; + } + } + } + } + Adw.StatusPage no_results { + title: _("No Results Found"); + description: _("Try a different search"); + icon-name: "system-search-symbolic"; + } + } + } + } + } } diff --git a/src/remotes_page/remotes_page.py b/src/remotes_page/remotes_page.py index 8f1b953..5d2a870 100644 --- a/src/remotes_page/remotes_page.py +++ b/src/remotes_page/remotes_page.py @@ -7,284 +7,284 @@ from .loading_status import LoadingStatus import subprocess class NewRemoteRow(Adw.ActionRow): - __gtype_name__ = "NewRemoteRow" + __gtype_name__ = "NewRemoteRow" - def idle_stuff(self, *args): - self.set_title(self.info["title"]) - self.set_subtitle(self.info["description"]) - self.add_suffix(Gtk.Image.new_from_icon_name("plus-large-symbolic")) + def idle_stuff(self, *args): + self.set_title(self.info["title"]) + self.set_subtitle(self.info["description"]) + self.add_suffix(Gtk.Image.new_from_icon_name("plus-large-symbolic")) - def __init__(self, info, **kwargs): - super().__init__(**kwargs) - self.info = info - GLib.idle_add(self.idle_stuff) - self.set_activatable(True) + def __init__(self, info, **kwargs): + super().__init__(**kwargs) + self.info = info + GLib.idle_add(self.idle_stuff) + self.set_activatable(True) @Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remotes_page.ui") class RemotesPage(Adw.NavigationPage): - # Preselected Remotes - new_remotes = [ - { - "title": "AppCenter", - "name": "appcenter", - "link": "https://flatpak.elementary.io/repo.flatpakrepo", - "description": _("The open source, pay-what-you-want app store from elementary") - }, - { - "title": "Flathub", - "name": "flathub", - "link": "https://dl.flathub.org/repo/flathub.flatpakrepo", - "description": _("Central repository of Flatpak applications"), - }, - { - "title": "Flathub beta", - "name": "flathub-beta", - "link": "https://flathub.org/beta-repo/flathub-beta.flatpakrepo", - "description": _("Beta builds of Flatpak applications"), - }, - { - "title": "Fedora", - "name": "fedora", - "link": "oci+https://registry.fedoraproject.org", - "description": _("Flatpaks packaged by Fedora Linux"), - }, - { - "title": "GNOME Nightly", - "name": "gnome-nightly", - "link": "https://nightly.gnome.org/gnome-nightly.flatpakrepo", - "description": _("The latest beta GNOME Apps and Runtimes"), - }, - { - "title": "WebKit Developer SDK", - "name": "webkit-sdk", - "link": "https://software.igalia.com/flatpak-refs/webkit-sdk.flatpakrepo", - "description": _("Central repository of the WebKit Developer and Runtime SDK"), - } - ] + # Preselected Remotes + new_remotes = [ + { + "title": "AppCenter", + "name": "appcenter", + "link": "https://flatpak.elementary.io/repo.flatpakrepo", + "description": _("The open source, pay-what-you-want app store from elementary") + }, + { + "title": "Flathub", + "name": "flathub", + "link": "https://dl.flathub.org/repo/flathub.flatpakrepo", + "description": _("Central repository of Flatpak applications"), + }, + { + "title": "Flathub beta", + "name": "flathub-beta", + "link": "https://flathub.org/beta-repo/flathub-beta.flatpakrepo", + "description": _("Beta builds of Flatpak applications"), + }, + { + "title": "Fedora", + "name": "fedora", + "link": "oci+https://registry.fedoraproject.org", + "description": _("Flatpaks packaged by Fedora Linux"), + }, + { + "title": "GNOME Nightly", + "name": "gnome-nightly", + "link": "https://nightly.gnome.org/gnome-nightly.flatpakrepo", + "description": _("The latest beta GNOME Apps and Runtimes"), + }, + { + "title": "WebKit Developer SDK", + "name": "webkit-sdk", + "link": "https://software.igalia.com/flatpak-refs/webkit-sdk.flatpakrepo", + "description": _("Central repository of the WebKit Developer and Runtime SDK"), + } + ] - __gtype_name__ = 'RemotesPage' - gtc = Gtk.Template.Child + __gtype_name__ = 'RemotesPage' + gtc = Gtk.Template.Child - search_button = gtc() - search_bar = gtc() - search_entry = gtc() - toast_overlay = gtc() - stack = gtc() - current_remotes_group = gtc() - show_disabled_button = gtc() - show_disabled_button_content = gtc() - new_remotes_group = gtc() - file_remote_row = gtc() - custom_remote_row = gtc() - none_visible = gtc() - status_stack = gtc() - loading_view = gtc() - adding_view = gtc() - main_view = gtc() + search_button = gtc() + search_bar = gtc() + search_entry = gtc() + toast_overlay = gtc() + stack = gtc() + current_remotes_group = gtc() + show_disabled_button = gtc() + show_disabled_button_content = gtc() + new_remotes_group = gtc() + file_remote_row = gtc() + custom_remote_row = gtc() + none_visible = gtc() + status_stack = gtc() + loading_view = gtc() + adding_view = gtc() + main_view = gtc() - no_results = gtc() - no_remotes = gtc() - content_page = gtc() + no_results = gtc() + no_remotes = gtc() + content_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 = "remotes" - - def start_loading(self): - self.search_button.set_active(False) - self.status_stack.set_visible_child(self.loading_view) - self.total_disabled = 0 - for row in self.current_remote_rows: - self.current_remotes_group.remove(row) + # 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 = "remotes" + + def start_loading(self): + self.search_button.set_active(False) + self.status_stack.set_visible_child(self.loading_view) + self.total_disabled = 0 + for row in self.current_remote_rows: + self.current_remotes_group.remove(row) - self.current_remote_rows.clear() + self.current_remote_rows.clear() - def end_loading(self): - show_disabled = self.show_disabled_button.get_active() - self.show_disabled_button.set_visible(False) - total_visible = 0 - for installation, remotes in HostInfo.remotes.items(): - for remote in remotes: - row = RemoteRow(self, installation, remote) - self.current_remote_rows.append(row) - self.current_remotes_group.add(row) - if row.remote.disabled: - self.total_disabled += 1 - self.show_disabled_button.set_visible(True) - if show_disabled: - total_visible += 1 - else: - row.set_visible(False) - else: - total_visible += 1 + def end_loading(self): + show_disabled = self.show_disabled_button.get_active() + self.show_disabled_button.set_visible(False) + total_visible = 0 + for installation, remotes in HostInfo.remotes.items(): + for remote in remotes: + row = RemoteRow(self, installation, remote) + self.current_remote_rows.append(row) + self.current_remotes_group.add(row) + if row.remote.disabled: + self.total_disabled += 1 + self.show_disabled_button.set_visible(True) + if show_disabled: + total_visible += 1 + else: + row.set_visible(False) + else: + total_visible += 1 - self.none_visible.set_visible(total_visible == 0) + self.none_visible.set_visible(total_visible == 0) - if len(self.current_remote_rows) == 0: - self.no_remotes.set_visible(True) - self.none_visible.set_visible(False) - else: - self.no_remotes.set_visible(False) + if len(self.current_remote_rows) == 0: + self.no_remotes.set_visible(True) + self.none_visible.set_visible(False) + else: + self.no_remotes.set_visible(False) - GLib.idle_add(lambda *_: self.status_stack.set_visible_child(self.main_view)) + GLib.idle_add(lambda *_: self.status_stack.set_visible_child(self.main_view)) - def none_visible_handler(self): - any_visible = False - for row in self.current_remote_rows: - if row.get_visible(): - any_visible = True - break - - self.none_visible.set_visible(not any_visible) + def none_visible_handler(self): + any_visible = False + for row in self.current_remote_rows: + if row.get_visible(): + any_visible = True + break + + self.none_visible.set_visible(not any_visible) - def filter_remote(self, row): - self.filter_setting.set_boolean("show-apps", True) - self.filter_setting.set_boolean("show-runtimes", True) - self.filter_setting.set_string("remotes-list", f"{row.remote.name}<>{row.installation};") - self.filter_setting.reset("runtimes-list") - packages_page = self.main_window.pages[self.main_window.packages_row] - packages_page.filters_page.generate_filters() - 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): - install = row.installation - cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-delete', row.remote.name, '--force'] - if install == "user" or install == "system": - cmd.append(f"--{install}") - else: - cmd.append(f"--installation={install}") + def filter_remote(self, row): + self.filter_setting.set_boolean("show-apps", True) + self.filter_setting.set_boolean("show-runtimes", True) + self.filter_setting.set_string("remotes-list", f"{row.remote.name}<>{row.installation};") + self.filter_setting.reset("runtimes-list") + packages_page = self.main_window.pages[self.main_window.packages_row] + packages_page.filters_page.generate_filters() + 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): + install = row.installation + cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-delete', row.remote.name, '--force'] + if install == "user" or install == "system": + cmd.append(f"--{install}") + else: + cmd.append(f"--installation={install}") - 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 + 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): - if error[0]: - self.toast_overlay.add_toast(ErrorToast(_("Could not remove remote"), str(error[0])).toast) - else: - filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page - filters_page.settings.reset("remotes-list") - filters_page.all_remotes_switch.set_active(False) - # filters_page.packages_page.apply_filters() - self.main_window.refresh_handler() - self.toast_overlay.add_toast(Adw.Toast(title=_("Removed {}").format(row.remote.title))) + def callback(*args): + if error[0]: + self.toast_overlay.add_toast(ErrorToast(_("Could not remove remote"), str(error[0])).toast) + else: + filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page + filters_page.settings.reset("remotes-list") + filters_page.all_remotes_switch.set_active(False) + # filters_page.packages_page.apply_filters() + self.main_window.refresh_handler() + self.toast_overlay.add_toast(Adw.Toast(title=_("Removed {}").format(row.remote.title))) - def on_response(_, response): - if response != "continue": - return - - Gio.Task.new(None, None, callback).run_in_thread(thread) + 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)) - dialog.add_response("cancel", _("Cancel")) - dialog.add_response("continue", _("Remove")) - dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) - dialog.connect("response", on_response) - dialog.present(self.main_window) + dialog = Adw.AlertDialog(heading=_("Remove {}?").format(row.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(row.remote.name)) + dialog.add_response("cancel", _("Cancel")) + dialog.add_response("continue", _("Remove")) + dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) + dialog.connect("response", on_response) + dialog.present(self.main_window) - def on_search(self, entry): - 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() - visible = (title_match or subtitle_match) and (show_disabled or not row.remote.disabled) - total += visible - row.set_visible(visible) + def on_search(self, entry): + 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() + visible = (title_match or subtitle_match) and (show_disabled or not row.remote.disabled) + total += visible + row.set_visible(visible) - if text == "": - self.stack.set_visible_child(self.content_page) - return + if text == "": + self.stack.set_visible_child(self.content_page) + 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] - info = { - "title": name.title(), - "name": name, - "description": "local file", - "link": path, - } - AddRemoteDialog(self.main_window, self, info).present(self.main_window) - except Exception as e: - self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast) + 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] + info = { + "title": name.title(), + "name": name, + "description": "local file", + "link": path, + } + AddRemoteDialog(self.main_window, self, info).present(self.main_window) + except Exception as e: + self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast) - def file_callback(self, chooser, result): - try: - file = chooser.open_finish(result) - path = file.get_path() - self.local_file_handler(path) - except GLib.GError as ge: - if "Dismissed by user" in str(ge): - return - self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(ge)).toast) - except Exception as e: - self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast) + def file_callback(self, chooser, result): + try: + file = chooser.open_finish(result) + path = file.get_path() + self.local_file_handler(path) + except GLib.GError as ge: + if "Dismissed by user" in str(ge): + return + self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(ge)).toast) + except Exception as e: + self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast) - def add_file_handler(self): - file_filter = Gtk.FileFilter(name=_("Flatpak Repos")) - file_filter.add_suffix("flatpakrepo") - filters = Gio.ListStore.new(Gtk.FileFilter) - filters.append(file_filter) - file_chooser = Gtk.FileDialog() - file_chooser.set_filters(filters) - file_chooser.set_default_filter(file_filter) - file_chooser.open(self.main_window, None, self.file_callback) + def add_file_handler(self): + file_filter = Gtk.FileFilter(name=_("Flatpak Repos")) + file_filter.add_suffix("flatpakrepo") + filters = Gio.ListStore.new(Gtk.FileFilter) + filters.append(file_filter) + file_chooser = Gtk.FileDialog() + file_chooser.set_filters(filters) + file_chooser.set_default_filter(file_filter) + file_chooser.open(self.main_window, None, self.file_callback) - def show_disabled_handler(self, button): - show_disabled = button.get_active() - self.show_disabled_button_content.set_icon_name("eye-open-negative-filled-symbolic" if show_disabled else "eye-not-looking-symbolic") - total_visible = 0 - for row in self.current_remote_rows: - if row.remote.disabled: - if show_disabled: # show disabled - row.set_visible(True) - total_visible += 1 - else: - row.set_visible(False) - else: - total_visible += 1 + def show_disabled_handler(self, button): + show_disabled = button.get_active() + self.show_disabled_button_content.set_icon_name("eye-open-negative-filled-symbolic" if show_disabled else "eye-not-looking-symbolic") + total_visible = 0 + for row in self.current_remote_rows: + if row.remote.disabled: + if show_disabled: # show disabled + row.set_visible(True) + total_visible += 1 + else: + row.set_visible(False) + else: + 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) + self.none_visible.set_visible(total_visible == 0) + + def new_custom_handler(self, *args): + AddRemoteDialog(self.main_window, self).present(self.main_window) - def __init__(self, main_window, **kwargs): - super().__init__(**kwargs) + def __init__(self, main_window, **kwargs): + super().__init__(**kwargs) - # Extra Object Creation - self.__class__.instance = self - self.main_window = main_window - self.search_bar.set_key_capture_widget(main_window) - self.current_remote_rows = [] - self.filter_setting = Gio.Settings.new("io.github.flattool.Warehouse.filter") - self.total_disabled = 0 + # Extra Object Creation + self.__class__.instance = self + self.main_window = main_window + self.search_bar.set_key_capture_widget(main_window) + self.current_remote_rows = [] + self.filter_setting = Gio.Settings.new("io.github.flattool.Warehouse.filter") + self.total_disabled = 0 - # Connections - self.file_remote_row.connect("activated", lambda *_: self.add_file_handler()) - self.custom_remote_row.connect("activated", self.new_custom_handler) - self.search_entry.connect("search-changed", self.on_search) - self.show_disabled_button.connect("toggled", self.show_disabled_handler) + # Connections + self.file_remote_row.connect("activated", lambda *_: self.add_file_handler()) + self.custom_remote_row.connect("activated", self.new_custom_handler) + self.search_entry.connect("search-changed", self.on_search) + self.show_disabled_button.connect("toggled", self.show_disabled_handler) - # Apply - self.adding_view.set_content(LoadingStatus(_("Adding Remote"), _("This should only take a moment"))) - self.loading_view.set_content(LoadingStatus(_("Loading Remotes"), _("This should only take a moment"))) - for item in self.new_remotes: - row = NewRemoteRow(item) - row.connect("activated", lambda *_, remote_info=item: AddRemoteDialog(main_window, self, remote_info).present(main_window)) - self.new_remotes_group.add(row) + # Apply + self.adding_view.set_content(LoadingStatus(_("Adding Remote"), _("This should only take a moment"))) + self.loading_view.set_content(LoadingStatus(_("Loading Remotes"), _("This should only take a moment"))) + for item in self.new_remotes: + row = NewRemoteRow(item) + row.connect("activated", lambda *_, remote_info=item: AddRemoteDialog(main_window, self, remote_info).present(main_window)) + self.new_remotes_group.add(row) diff --git a/src/snapshot_page/new_snapshot_dialog.blp b/src/snapshot_page/new_snapshot_dialog.blp index 31f0eb5..16c3759 100644 --- a/src/snapshot_page/new_snapshot_dialog.blp +++ b/src/snapshot_page/new_snapshot_dialog.blp @@ -2,93 +2,93 @@ using Gtk 4.0; using Adw 1; template $NewSnapshotDialog : Adw.Dialog { - follows-content-size: true; - Adw.ToastOverlay toast_overlay { - Adw.NavigationPage nav_page { - title: "No Title Set"; - Adw.ToolbarView { - [top] - Adw.HeaderBar { - show-start-title-buttons: false; - show-end-title-buttons: false; - [start] - Button list_cancel_button { - label: _("Cancel"); - } - [start] - ToggleButton search_button { - icon-name: "loupe-large-symbolic"; - tooltip-text: _("Search Apps"); - } - [end] - Button create_button { - sensitive: false; - label: _("Create"); - styles ["suggested-action"] - } - } - [top] - Adw.Clamp { - SearchBar search_bar { - search-mode-enabled: bind search_button.active bidirectional; - key-capture-widget: template; - SearchEntry search_entry { - hexpand: true; - placeholder-text: _("Search Apps"); - } - } - } - Stack stack { - ScrolledWindow scrolled_window { - propagate-natural-height: true; - propagate-natural-width: true; - Box { - orientation: vertical; - Adw.EntryRow name_entry { - title: "No Title Set"; - margin-start: 12; - margin-end: 12; - margin-top: 12; - margin-bottom: 12; - styles ["card"] - } - ListBox listbox { - valign: start; - margin-start: 12; - margin-end: 12; - // margin-top: 12; - margin-bottom: 12; - selection-mode: none; - styles ["boxed-list"] - } - } - } - Adw.StatusPage no_results { - title: _("No Results Found"); - description: _("Try a different search"); - icon-name: "system-search-symbolic"; - } - } - [bottom] - ActionBar { - revealed: bind search_button.visible; - [start] - Button select_all_button { - styles ["raised"] - Adw.ButtonContent { - label: _("Select All"); - icon-name: "selection-mode-symbolic"; - } - } - [end] - Label total_selected_label { - label: ""; - ellipsize: middle; - margin-end: 6; - visible: false; - } - } - } - } - } + follows-content-size: true; + Adw.ToastOverlay toast_overlay { + Adw.NavigationPage nav_page { + title: "No Title Set"; + Adw.ToolbarView { + [top] + Adw.HeaderBar { + show-start-title-buttons: false; + show-end-title-buttons: false; + [start] + Button list_cancel_button { + label: _("Cancel"); + } + [start] + ToggleButton search_button { + icon-name: "loupe-large-symbolic"; + tooltip-text: _("Search Apps"); + } + [end] + Button create_button { + sensitive: false; + label: _("Create"); + styles ["suggested-action"] + } + } + [top] + Adw.Clamp { + SearchBar search_bar { + search-mode-enabled: bind search_button.active bidirectional; + key-capture-widget: template; + SearchEntry search_entry { + hexpand: true; + placeholder-text: _("Search Apps"); + } + } + } + Stack stack { + ScrolledWindow scrolled_window { + propagate-natural-height: true; + propagate-natural-width: true; + Box { + orientation: vertical; + Adw.EntryRow name_entry { + title: "No Title Set"; + margin-start: 12; + margin-end: 12; + margin-top: 12; + margin-bottom: 12; + styles ["card"] + } + ListBox listbox { + valign: start; + margin-start: 12; + margin-end: 12; + // margin-top: 12; + margin-bottom: 12; + selection-mode: none; + styles ["boxed-list"] + } + } + } + Adw.StatusPage no_results { + title: _("No Results Found"); + description: _("Try a different search"); + icon-name: "system-search-symbolic"; + } + } + [bottom] + ActionBar { + revealed: bind search_button.visible; + [start] + Button select_all_button { + styles ["raised"] + Adw.ButtonContent { + label: _("Select All"); + icon-name: "selection-mode-symbolic"; + } + } + [end] + Label total_selected_label { + label: ""; + ellipsize: middle; + margin-end: 6; + visible: false; + } + } + } + } + } } diff --git a/src/snapshot_page/new_snapshot_dialog.py b/src/snapshot_page/new_snapshot_dialog.py index 7145b6e..9e5aef9 100644 --- a/src/snapshot_page/new_snapshot_dialog.py +++ b/src/snapshot_page/new_snapshot_dialog.py @@ -7,197 +7,197 @@ import os, time @Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/new_snapshot_dialog.ui") class NewSnapshotDialog(Adw.Dialog): - __gtype_name__ = "NewSnapshotDialog" - gtc = Gtk.Template.Child - - toast_overlay = gtc() - nav_page = gtc() - list_cancel_button = gtc() - search_button = gtc() - create_button = gtc() - search_entry = gtc() - name_entry = gtc() - listbox = gtc() - select_all_button = gtc() - total_selected_label = gtc() - scrolled_window = gtc() - 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() - search = self.search_entry.get_text().lower() - if search in title or search in subtitle: - self.is_result = True - 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 - text_good = len(text) > 0 and not("/" in text or "\0" in text) - self.create_button.set_sensitive(something_selected and text_good) - if text_good: - 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 - for worker in self.workers: - 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) - self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.snapshotting_view) - self.workers.clear() - 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, - new_path=f"{HostInfo.snapshots_path}{package.info['id']}", - file_name=f"{int(time.time())}_{package.info["version"]}", - name=self.name_entry.get_text(), - toast_overlay=self.snapshot_page.toast_overlay, - ) - 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() - if self.is_result: - 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 - self.on_done = on_done - self.is_result = False - self.rows = [] - self.selected_rows = [] - self.workers = [] - self.packages = packages - - # Connections - self.connect("closed", self.on_close) - self.create_button.connect("clicked", self.on_create) - self.search_entry.connect("search-changed", self.on_invalidate) - self.list_cancel_button.connect("clicked", lambda *_: self.close()) - 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) - self.name_entry.set_title(_("Name these Snapshots")) - if not packages is None: - self.search_entry.set_editable(False) - self.search_button.set_visible(False) - self.nav_page.set_title(_("New Snapshot")) - self.set_packages() - self.no_results.set_visible(False) - if len(packages) == 1: - self.name_entry.set_title(_("Name this Snapshot")) - else: - self.nav_page.set_title(_("New Snapshots")) - self.generate_list() + __gtype_name__ = "NewSnapshotDialog" + gtc = Gtk.Template.Child + + toast_overlay = gtc() + nav_page = gtc() + list_cancel_button = gtc() + search_button = gtc() + create_button = gtc() + search_entry = gtc() + name_entry = gtc() + listbox = gtc() + select_all_button = gtc() + total_selected_label = gtc() + scrolled_window = gtc() + 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() + search = self.search_entry.get_text().lower() + if search in title or search in subtitle: + self.is_result = True + 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 + text_good = len(text) > 0 and not("/" in text or "\0" in text) + self.create_button.set_sensitive(something_selected and text_good) + if text_good: + 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 + for worker in self.workers: + 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) + self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.snapshotting_view) + self.workers.clear() + 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, + new_path=f"{HostInfo.snapshots_path}{package.info['id']}", + file_name=f"{int(time.time())}_{package.info["version"]}", + name=self.name_entry.get_text(), + toast_overlay=self.snapshot_page.toast_overlay, + ) + 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() + if self.is_result: + 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 + self.on_done = on_done + self.is_result = False + self.rows = [] + self.selected_rows = [] + self.workers = [] + self.packages = packages + + # Connections + self.connect("closed", self.on_close) + self.create_button.connect("clicked", self.on_create) + self.search_entry.connect("search-changed", self.on_invalidate) + self.list_cancel_button.connect("clicked", lambda *_: self.close()) + 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) + self.name_entry.set_title(_("Name these Snapshots")) + if not packages is None: + self.search_entry.set_editable(False) + self.search_button.set_visible(False) + self.nav_page.set_title(_("New Snapshot")) + self.set_packages() + self.no_results.set_visible(False) + if len(packages) == 1: + self.name_entry.set_title(_("Name this Snapshot")) + else: + self.nav_page.set_title(_("New Snapshots")) + self.generate_list() diff --git a/src/snapshot_page/snapshot_box.blp b/src/snapshot_page/snapshot_box.blp index 53db485..4960f9e 100644 --- a/src/snapshot_page/snapshot_box.blp +++ b/src/snapshot_page/snapshot_box.blp @@ -2,98 +2,98 @@ using Gtk 4.0; using Adw 1; template $SnapshotBox : Gtk.Box { - orientation: vertical; - spacing: 6; - Box { - margin-start: 12; - margin-end: 12; - margin-top: 6; - spacing: 12; - Box { - orientation: vertical; - Label title { - label: _("No Name Set"); - wrap: true; - wrap-mode: word_char; - justify: left; - halign: start; - styles ["title-4"] - } - Label date { - label: _("No date found"); - wrap: true; - justify: left; - halign: start; - } - } - Label version { - label: _("No version found"); - wrap: true; - justify: right; - hexpand: true; - halign: end; - natural-wrap-mode: none; - } - } - Box { - margin-start: 6; - margin-end: 6; - margin-bottom: 6; - spacing: 3; - homogeneous: true; - Button apply_button { - Adw.ButtonContent { - label: _("Apply"); - icon-name: "check-plain-symbolic"; - can-shrink: true; - } - hexpand: true; - styles ["flat"] - } - MenuButton rename_button { - Adw.ButtonContent { - label: _("Rename"); - icon-name: "edit-symbolic"; - can-shrink: true; - } - hexpand: true; - styles ["flat"] - popover: rename_menu; - } - Button trash_button { - Adw.ButtonContent { - label: _("Trash"); - icon-name: "user-trash-symbolic"; - can-shrink: true; - } - hexpand: true; - styles ["flat"] - } - } + orientation: vertical; + spacing: 6; + Box { + margin-start: 12; + margin-end: 12; + margin-top: 6; + spacing: 12; + Box { + orientation: vertical; + Label title { + label: _("No Name Set"); + wrap: true; + wrap-mode: word_char; + justify: left; + halign: start; + styles ["title-4"] + } + Label date { + label: _("No date found"); + wrap: true; + justify: left; + halign: start; + } + } + Label version { + label: _("No version found"); + wrap: true; + justify: right; + hexpand: true; + halign: end; + natural-wrap-mode: none; + } + } + Box { + margin-start: 6; + margin-end: 6; + margin-bottom: 6; + spacing: 3; + homogeneous: true; + Button apply_button { + Adw.ButtonContent { + label: _("Apply"); + icon-name: "check-plain-symbolic"; + can-shrink: true; + } + hexpand: true; + styles ["flat"] + } + MenuButton rename_button { + Adw.ButtonContent { + label: _("Rename"); + icon-name: "edit-symbolic"; + can-shrink: true; + } + hexpand: true; + styles ["flat"] + popover: rename_menu; + } + Button trash_button { + Adw.ButtonContent { + label: _("Trash"); + icon-name: "user-trash-symbolic"; + can-shrink: true; + } + hexpand: true; + styles ["flat"] + } + } } Popover rename_menu { - Box { - orientation: vertical; - spacing: 11; - margin-start: 12; - margin-end: 12; - margin-top: 5; - margin-bottom: 12; - Label { - label: _("Rename Snapshot?"); - styles ["title-2"] - } - Box { - spacing: 6; - Entry rename_entry { - text: bind title.label; - } - Button apply_rename { - icon-name: "check-plain-symbolic"; - tooltip-text: _("Confirm Rename"); - styles ["circular", "suggested-action"] - } - } - } + Box { + orientation: vertical; + spacing: 11; + margin-start: 12; + margin-end: 12; + margin-top: 5; + margin-bottom: 12; + Label { + label: _("Rename Snapshot?"); + styles ["title-2"] + } + Box { + spacing: 6; + Entry rename_entry { + text: bind title.label; + } + Button apply_rename { + icon-name: "check-plain-symbolic"; + tooltip-text: _("Confirm Rename"); + styles ["circular", "suggested-action"] + } + } + } } diff --git a/src/snapshot_page/snapshot_box.py b/src/snapshot_page/snapshot_box.py index 4a179d8..200cda5 100644 --- a/src/snapshot_page/snapshot_box.py +++ b/src/snapshot_page/snapshot_box.py @@ -6,184 +6,184 @@ import os, subprocess, json @Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/snapshot_box.ui") class SnapshotBox(Gtk.Box): - __gtype_name__ = "SnapshotBox" - gtc = Gtk.Template.Child - - title = gtc() - date = gtc() - version = gtc() - apply_button = gtc() - rename_button = gtc() - rename_menu = gtc() - rename_entry = gtc() - apply_rename = gtc() - trash_button = gtc() - - def create_json(self): - try: - data = { - 'snapshot_version': 1, - 'name': '', - } - 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: - data = json.load(file) - data[key] = value - 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) - name = data['name'] - if name != "": - 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 - self.apply_rename.set_sensitive(valid) - if valid: - 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) - except subprocess.CalledProcessError as cpe: - 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")) - dialog.add_response("continue", _("Trash")) - 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) - if self.worker.stop: - self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.split_view) - self.parent_page.set_snapshots(self.parent_page.package_or_folder, True) - properties_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].properties_page - properties_page.set_properties(properties_page.package, True) - data_page = HostInfo.main_window.pages[HostInfo.main_window.user_data_row] - data_page.start_loading() - 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) - self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.snapshotting_view) - 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?"), - body=_("Any current user data for this app will be trashed") if has_data else "", - ) - dialog.add_response("cancel", _("Cancel")) - 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() - self.worker = TarWorker( - existing_path=f"{snapshots_path}{folder}", - 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 - self.epoch = int(split_folder[0]) - date_data = GLib.DateTime.new_from_unix_local(self.epoch).format("%x %X") - self.date.set_label(date_data) - self.version.set_label(_("Version: {}").format(split_folder[1].replace(".tar.zst", ""))) - self.json_path = f"{snapshots_path}{folder.replace('tar.zst', 'json')}" - self.load_from_json() - self.apply_button.connect("clicked", self.on_apply) - self.apply_rename.connect("clicked", self.on_rename) - self.rename_entry.connect("activate", self.on_rename) - self.rename_entry.connect("changed", self.valid_checker) - self.trash_button.connect("clicked", self.on_trash) + __gtype_name__ = "SnapshotBox" + gtc = Gtk.Template.Child + + title = gtc() + date = gtc() + version = gtc() + apply_button = gtc() + rename_button = gtc() + rename_menu = gtc() + rename_entry = gtc() + apply_rename = gtc() + trash_button = gtc() + + def create_json(self): + try: + data = { + 'snapshot_version': 1, + 'name': '', + } + 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: + data = json.load(file) + data[key] = value + 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) + name = data['name'] + if name != "": + 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 + self.apply_rename.set_sensitive(valid) + if valid: + 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) + except subprocess.CalledProcessError as cpe: + 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")) + dialog.add_response("continue", _("Trash")) + 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) + if self.worker.stop: + self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.split_view) + self.parent_page.set_snapshots(self.parent_page.package_or_folder, True) + properties_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].properties_page + properties_page.set_properties(properties_page.package, True) + data_page = HostInfo.main_window.pages[HostInfo.main_window.user_data_row] + data_page.start_loading() + 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) + self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.snapshotting_view) + 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?"), + body=_("Any current user data for this app will be trashed") if has_data else "", + ) + dialog.add_response("cancel", _("Cancel")) + 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() + self.worker = TarWorker( + existing_path=f"{snapshots_path}{folder}", + 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 + self.epoch = int(split_folder[0]) + date_data = GLib.DateTime.new_from_unix_local(self.epoch).format("%x %X") + self.date.set_label(date_data) + self.version.set_label(_("Version: {}").format(split_folder[1].replace(".tar.zst", ""))) + self.json_path = f"{snapshots_path}{folder.replace('tar.zst', 'json')}" + self.load_from_json() + self.apply_button.connect("clicked", self.on_apply) + self.apply_rename.connect("clicked", self.on_rename) + self.rename_entry.connect("activate", self.on_rename) + self.rename_entry.connect("changed", self.valid_checker) + self.trash_button.connect("clicked", self.on_trash) diff --git a/src/snapshot_page/snapshot_page.blp b/src/snapshot_page/snapshot_page.blp index 1963a8e..9300d83 100644 --- a/src/snapshot_page/snapshot_page.blp +++ b/src/snapshot_page/snapshot_page.blp @@ -2,234 +2,234 @@ using Gtk 4.0; using Adw 1; template $SnapshotPage : Adw.BreakpointBin { - width-request: 1; + width-request: 1; height-request: 1; - Adw.Breakpoint bp1 { + Adw.Breakpoint bp1 { condition ("max-width: 600") setters { split_view.collapsed: true; - split_view.show-content: false; + split_view.show-content: false; } } - Adw.NavigationPage { - title: _("Snapshots"); - Adw.ToastOverlay toast_overlay { - Stack status_stack { - Adw.NavigationSplitView split_view { - sidebar-width-fraction: 0.5; - max-sidebar-width: 999999999; - sidebar: - Adw.NavigationPage sidebar_navpage { - title: _("Snapshots"); - Adw.ToolbarView sidebar_tbv { - [top] - Adw.HeaderBar header_bar { - [start] - $SidebarButton {} - [start] - ToggleButton search_button { - icon-name: "loupe-large-symbolic"; - tooltip-text: _("Search Packages"); - } - [end] - Button new_button { - icon-name: "plus-large-symbolic"; - tooltip-text: _("New Snapshot"); - } - [end] - ToggleButton select_button { - icon-name: "selection-mode-symbolic"; - tooltip-text: _("Select Packages"); - } - } - [top] - SearchBar search_bar { - search-mode-enabled: bind search_button.active bidirectional; - SearchEntry search_entry { - hexpand: true; - placeholder-text: _("Search Snapshots"); - } - } - Stack stack { - Adw.StatusPage no_results { - title: _("No Results Found"); - description: _("Try a different search"); - icon-name: "system-search-symbolic"; - } - ScrolledWindow scrolled_window { - Box { - orientation: vertical; - - Box active_box { - orientation: vertical; - - Label { - label: _("Active Snapshots"); - halign: start; - styles ["heading"] - margin-top: 3; - margin-bottom: 6; - margin-start: 12; - margin-end: 12; - wrap: true; - wrap-mode: word_char; - } - Label { - label: _("Snapshots of installed apps"); - halign: start; - styles ["dim-label"] - margin-start: 12; - margin-end: 12; - margin-bottom: 3; - wrap: true; - wrap-mode: word_char; - } - ListBox active_listbox { - styles ["navigation-sidebar"] - valign: start; - } - } - Box leftover_box { - orientation: vertical; - - Label { - label: _("Leftover Snapshots"); - halign: start; - styles ["heading"] - margin-top: 3; - margin-bottom: 6; - margin-start: 12; - margin-end: 12; - wrap: true; - wrap-mode: word_char; - } - Label { - label: _("Snapshots of apps that are no longer installed"); - halign: start; - styles ["dim-label"] - margin-start: 12; - margin-end: 12; - margin-bottom: 3; - wrap: true; - wrap-mode: word_char; - } - ListBox leftover_listbox { - styles ["navigation-sidebar"] - valign: start; - } - } - } - } - } - [bottom] - Revealer { - reveal-child: bind select_button.active; - transition-type: slide_up; - [center] - Box bottom_bar { - styles ["toolbar"] - hexpand: true; - homogeneous: true; - Button select_all_button { - styles ["raised"] - Adw.ButtonContent { - icon-name: "selection-mode-symbolic"; - label: _("Select All"); - can-shrink: true; - } - } - Button copy_button { - sensitive: false; - styles ["raised"] - Adw.ButtonContent { - icon-name: "edit-copy-symbolic"; - label: _("Copy"); - can-shrink: true; - } - } - MenuButton more_button { - sensitive: false; - popover: more_popover; - styles ["raised"] - Adw.ButtonContent { - icon-name: "view-more-symbolic"; - label: _("More"); - can-shrink: true; - } - } - } - } - } - } - ; - } - Adw.ToolbarView no_snapshots { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - [start] - Button status_open_button { - icon-name: "folder-open-symbolic"; - tooltip-text: _("Open Snapshots Folder"); - } - } - Adw.ToastOverlay no_snapshots_toast { - Adw.StatusPage { - title: _("No Snapshots"); - description: _("Create a Snapshot to save the state of any Flatpak application"); - icon-name: "snapshots-alt-symbolic"; - Button status_new_button { - styles ["suggested-action", "pill"] - halign: center; - Adw.ButtonContent { - icon-name: "plus-large-symbolic"; - label: _("New Snapshot"); - } - } - } - } - } - Adw.ToolbarView loading_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.ToolbarView snapshotting_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - } - } - } + Adw.NavigationPage { + title: _("Snapshots"); + Adw.ToastOverlay toast_overlay { + Stack status_stack { + Adw.NavigationSplitView split_view { + sidebar-width-fraction: 0.5; + max-sidebar-width: 999999999; + sidebar: + Adw.NavigationPage sidebar_navpage { + title: _("Snapshots"); + Adw.ToolbarView sidebar_tbv { + [top] + Adw.HeaderBar header_bar { + [start] + $SidebarButton {} + [start] + ToggleButton search_button { + icon-name: "loupe-large-symbolic"; + tooltip-text: _("Search Packages"); + } + [end] + Button new_button { + icon-name: "plus-large-symbolic"; + tooltip-text: _("New Snapshot"); + } + [end] + ToggleButton select_button { + icon-name: "selection-mode-symbolic"; + tooltip-text: _("Select Packages"); + } + } + [top] + SearchBar search_bar { + search-mode-enabled: bind search_button.active bidirectional; + SearchEntry search_entry { + hexpand: true; + placeholder-text: _("Search Snapshots"); + } + } + Stack stack { + Adw.StatusPage no_results { + title: _("No Results Found"); + description: _("Try a different search"); + icon-name: "system-search-symbolic"; + } + ScrolledWindow scrolled_window { + Box { + orientation: vertical; + + Box active_box { + orientation: vertical; + + Label { + label: _("Active Snapshots"); + halign: start; + styles ["heading"] + margin-top: 3; + margin-bottom: 6; + margin-start: 12; + margin-end: 12; + wrap: true; + wrap-mode: word_char; + } + Label { + label: _("Snapshots of installed apps"); + halign: start; + styles ["dim-label"] + margin-start: 12; + margin-end: 12; + margin-bottom: 3; + wrap: true; + wrap-mode: word_char; + } + ListBox active_listbox { + styles ["navigation-sidebar"] + valign: start; + } + } + Box leftover_box { + orientation: vertical; + + Label { + label: _("Leftover Snapshots"); + halign: start; + styles ["heading"] + margin-top: 3; + margin-bottom: 6; + margin-start: 12; + margin-end: 12; + wrap: true; + wrap-mode: word_char; + } + Label { + label: _("Snapshots of apps that are no longer installed"); + halign: start; + styles ["dim-label"] + margin-start: 12; + margin-end: 12; + margin-bottom: 3; + wrap: true; + wrap-mode: word_char; + } + ListBox leftover_listbox { + styles ["navigation-sidebar"] + valign: start; + } + } + } + } + } + [bottom] + Revealer { + reveal-child: bind select_button.active; + transition-type: slide_up; + [center] + Box bottom_bar { + styles ["toolbar"] + hexpand: true; + homogeneous: true; + Button select_all_button { + styles ["raised"] + Adw.ButtonContent { + icon-name: "selection-mode-symbolic"; + label: _("Select All"); + can-shrink: true; + } + } + Button copy_button { + sensitive: false; + styles ["raised"] + Adw.ButtonContent { + icon-name: "edit-copy-symbolic"; + label: _("Copy"); + can-shrink: true; + } + } + MenuButton more_button { + sensitive: false; + popover: more_popover; + styles ["raised"] + Adw.ButtonContent { + icon-name: "view-more-symbolic"; + label: _("More"); + can-shrink: true; + } + } + } + } + } + } + ; + } + Adw.ToolbarView no_snapshots { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + [start] + Button status_open_button { + icon-name: "folder-open-symbolic"; + tooltip-text: _("Open Snapshots Folder"); + } + } + Adw.ToastOverlay no_snapshots_toast { + Adw.StatusPage { + title: _("No Snapshots"); + description: _("Create a Snapshot to save the state of any Flatpak application"); + icon-name: "snapshots-alt-symbolic"; + Button status_new_button { + styles ["suggested-action", "pill"] + halign: center; + Adw.ButtonContent { + icon-name: "plus-large-symbolic"; + label: _("New Snapshot"); + } + } + } + } + } + Adw.ToolbarView loading_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.ToolbarView snapshotting_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + } + } + } } Popover more_popover { - styles ["menu"] - ListBox more_menu { - Label new_snapshots { - label: _("Snapshot Apps"); - halign: start; - } - Label install_from_snapshots { - label: _("Install Apps"); - halign: start; - } - Label apply_snapshots { - label: _("Apply Snapshots"); - halign: start; - } - Label trash_snapshots { - label: _("Trash Snapshots"); - halign: start; - } - } + styles ["menu"] + ListBox more_menu { + Label new_snapshots { + label: _("Snapshot Apps"); + halign: start; + } + Label install_from_snapshots { + label: _("Install Apps"); + halign: start; + } + Label apply_snapshots { + label: _("Apply Snapshots"); + halign: start; + } + Label trash_snapshots { + label: _("Trash Snapshots"); + halign: start; + } + } } diff --git a/src/snapshot_page/snapshot_page.py b/src/snapshot_page/snapshot_page.py index a4486c8..20d9bca 100644 --- a/src/snapshot_page/snapshot_page.py +++ b/src/snapshot_page/snapshot_page.py @@ -11,564 +11,564 @@ from .attempt_install_dialog import AttemptInstallDialog 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) - self.check_button.add_css_class("selection-mode") - 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) - + __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) + self.check_button.add_css_class("selection-mode") + 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() - select_button = gtc() - search_entry = gtc() - search_bar = gtc() - active_box = gtc() - active_listbox = gtc() - leftover_box = gtc() - leftover_listbox = gtc() - split_view = gtc() - stack = gtc() - no_snapshots = gtc() - no_results = gtc() - scrolled_window = gtc() - status_open_button = gtc() - status_new_button = gtc() - new_button = gtc() - status_stack = gtc() - loading_view = gtc() - snapshotting_view = gtc() - select_all_button = gtc() - copy_button = gtc() - more_button = gtc() - more_popover = gtc() - more_menu = gtc() - new_snapshots = gtc() - 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 - instance = None - 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) - self.active_select_handler(None, row, False, True) - 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): - i += 1 - if row.package is package: - self.active_listbox.select_row(row) - self.active_select_handler(None, row, True) - self.toast_overlay.add_toast(Adw.Toast(title=_("Showing snapshots for {}").format(package.info['name']))) - break - else: - dialog = NewSnapshotDialog(self, self.snapshotting_status, self.refresh, [package]) - 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) - self.workers.clear() - self.select_button.set_active(False) - self.status_stack.set_visible_child(self.loading_view) - self.active_box.set_visible(True) - self.active_listbox.remove_all() - self.leftover_box.set_visible(True) - 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() - self.generate_leftover_list() - if (not self.active_box.get_visible()) and (not self.leftover_box.get_visible()): - GLib.idle_add(lambda *_: self.status_stack.set_visible_child(self.no_snapshots)) - else: - 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) - self.status_new_button.set_sensitive(True) - self.status_new_button.set_tooltip_text(None) - else: - self.new_button.set_sensitive(False) - 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 - total_active_visible = 0 - while row := self.active_listbox.get_row_at_index(i): - i += 1 - row.set_visible(False) - if text in row.get_title().lower(): - 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): - i += 1 - row.set_visible(False) - if text in row.get_title().lower(): - 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) - self.leftover_listbox.set_selection_mode(Gtk.SelectionMode.NONE if enable else Gtk.SelectionMode.SINGLE) - if self.last_activated_row in self.active_listbox: - 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(): - self.selected_active_rows.append(row) - elif row in self.selected_active_rows: - self.selected_active_rows.remove(row) - elif type(row) is LeftoverSnapshotRow: - if row.check_button.get_active(): - 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 - self.sidebar_navpage.set_title(_("{} Selected").format(total_active + total_leftover) if total > 0 else _("Snapshots")) - self.new_snapshots.set_visible(total_active > 0) - self.copy_button.set_sensitive(total > 0) - self.more_button.set_sensitive(total > 0) - i = 0 - while row := self.more_menu.get_row_at_index(i): - i += 1 - match row.get_child(): - case self.new_snapshots: - row.set_visible(total_active > 0 and total_leftover == 0) - case self.apply_snapshots: - 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 - while row := self.active_listbox.get_row_at_index(i): - 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 - for worker in self.workers: - 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 - for tar in id_to_tar[app_id]: - 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}", - new_path=f"{HostInfo.home}/.var/app/{app_id}/", - toast_overlay=self.toast_overlay, - ) - 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) - self.snapshotting_status.progress_label.set_visible(len(self.workers) > 1) - self.status_stack.set_visible_child(self.snapshotting_view) - 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() - self.end_loading() - 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")) - dialog.add_response("continue", _("Trash")) - 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() - match row: - case self.new_snapshots: - self.select_new_handler() - case self.apply_snapshots: - self.select_apply_handler() - case self.install_from_snapshots: - 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 - self.active_snapshot_paks = [] - self.selected_active_rows = [] - self.selected_leftover_rows = [] - self.workers = [] - self.leftover_snapshots = [] - self.list_page = SnapshotsListPage(self) - self.snapshotting_status = LoadingStatus("Initial Title", _("This could take a while"), True, self.on_cancel) - 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"))) - self.snapshotting_view.set_content(self.snapshotting_status) - 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) - self.status_open_button.connect("clicked", self.open_snapshots_folder) - self.status_new_button.connect("clicked", self.on_new) - self.new_button.connect("clicked", self.on_new) - self.search_entry.connect("search-changed", self.on_search) - self.select_button.connect("toggled", self.set_selection_mode) - self.select_all_button.connect("clicked", self.select_all_handler) - self.copy_button.connect("clicked", self.select_copy_handler) - self.more_menu.connect("row-activated", self.more_menu_handler) + __gtype_name__ = "SnapshotPage" + gtc = Gtk.Template.Child + + toast_overlay = gtc() + sidebar_navpage = gtc() + search_button = gtc() + select_button = gtc() + search_entry = gtc() + search_bar = gtc() + active_box = gtc() + active_listbox = gtc() + leftover_box = gtc() + leftover_listbox = gtc() + split_view = gtc() + stack = gtc() + no_snapshots = gtc() + no_results = gtc() + scrolled_window = gtc() + status_open_button = gtc() + status_new_button = gtc() + new_button = gtc() + status_stack = gtc() + loading_view = gtc() + snapshotting_view = gtc() + select_all_button = gtc() + copy_button = gtc() + more_button = gtc() + more_popover = gtc() + more_menu = gtc() + new_snapshots = gtc() + 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 + instance = None + 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) + self.active_select_handler(None, row, False, True) + 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): + i += 1 + if row.package is package: + self.active_listbox.select_row(row) + self.active_select_handler(None, row, True) + self.toast_overlay.add_toast(Adw.Toast(title=_("Showing snapshots for {}").format(package.info['name']))) + break + else: + dialog = NewSnapshotDialog(self, self.snapshotting_status, self.refresh, [package]) + 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) + self.workers.clear() + self.select_button.set_active(False) + self.status_stack.set_visible_child(self.loading_view) + self.active_box.set_visible(True) + self.active_listbox.remove_all() + self.leftover_box.set_visible(True) + 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() + self.generate_leftover_list() + if (not self.active_box.get_visible()) and (not self.leftover_box.get_visible()): + GLib.idle_add(lambda *_: self.status_stack.set_visible_child(self.no_snapshots)) + else: + 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) + self.status_new_button.set_sensitive(True) + self.status_new_button.set_tooltip_text(None) + else: + self.new_button.set_sensitive(False) + 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 + total_active_visible = 0 + while row := self.active_listbox.get_row_at_index(i): + i += 1 + row.set_visible(False) + if text in row.get_title().lower(): + 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): + i += 1 + row.set_visible(False) + if text in row.get_title().lower(): + 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) + self.leftover_listbox.set_selection_mode(Gtk.SelectionMode.NONE if enable else Gtk.SelectionMode.SINGLE) + if self.last_activated_row in self.active_listbox: + 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(): + self.selected_active_rows.append(row) + elif row in self.selected_active_rows: + self.selected_active_rows.remove(row) + elif type(row) is LeftoverSnapshotRow: + if row.check_button.get_active(): + 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 + self.sidebar_navpage.set_title(_("{} Selected").format(total_active + total_leftover) if total > 0 else _("Snapshots")) + self.new_snapshots.set_visible(total_active > 0) + self.copy_button.set_sensitive(total > 0) + self.more_button.set_sensitive(total > 0) + i = 0 + while row := self.more_menu.get_row_at_index(i): + i += 1 + match row.get_child(): + case self.new_snapshots: + row.set_visible(total_active > 0 and total_leftover == 0) + case self.apply_snapshots: + 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 + while row := self.active_listbox.get_row_at_index(i): + 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 + for worker in self.workers: + 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 + for tar in id_to_tar[app_id]: + 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}", + new_path=f"{HostInfo.home}/.var/app/{app_id}/", + toast_overlay=self.toast_overlay, + ) + 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) + self.snapshotting_status.progress_label.set_visible(len(self.workers) > 1) + self.status_stack.set_visible_child(self.snapshotting_view) + 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() + self.end_loading() + 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")) + dialog.add_response("continue", _("Trash")) + 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() + match row: + case self.new_snapshots: + self.select_new_handler() + case self.apply_snapshots: + self.select_apply_handler() + case self.install_from_snapshots: + 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 + self.active_snapshot_paks = [] + self.selected_active_rows = [] + self.selected_leftover_rows = [] + self.workers = [] + self.leftover_snapshots = [] + self.list_page = SnapshotsListPage(self) + self.snapshotting_status = LoadingStatus("Initial Title", _("This could take a while"), True, self.on_cancel) + 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"))) + self.snapshotting_view.set_content(self.snapshotting_status) + 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) + self.status_open_button.connect("clicked", self.open_snapshots_folder) + self.status_new_button.connect("clicked", self.on_new) + self.new_button.connect("clicked", self.on_new) + self.search_entry.connect("search-changed", self.on_search) + self.select_button.connect("toggled", self.set_selection_mode) + self.select_all_button.connect("clicked", self.select_all_handler) + self.copy_button.connect("clicked", self.select_copy_handler) + self.more_menu.connect("row-activated", self.more_menu_handler) diff --git a/src/snapshot_page/snapshots_list_page.blp b/src/snapshot_page/snapshots_list_page.blp index 384094d..0d3920d 100644 --- a/src/snapshot_page/snapshots_list_page.blp +++ b/src/snapshot_page/snapshots_list_page.blp @@ -2,46 +2,46 @@ using Gtk 4.0; using Adw 1; template $SnapshotsListPage : Adw.NavigationPage { - title: _("Snapshots List"); - Adw.ToastOverlay toast_overlay { - Adw.ToolbarView toolbar_view { - [top] - Adw.HeaderBar { - [start] - Button open_button { - icon-name: "folder-open-symbolic"; - tooltip-text: _("Open Snapshots Folder for this App"); - } - } - ScrolledWindow { - Adw.Clamp { - margin-start: 12; - margin-end: 12; - margin-top: 12; - margin-bottom: 12; - ListBox listbox { - valign: start; - selection-mode: none; - styles ["boxed-list"] - Adw.PreferencesGroup { - Adw.ActionRow {title: "test";} - } - } - } - } - [bottom] - ActionBar { - [center] - Button new_button { - margin-top: 3; - margin-bottom: 3; - styles ["pill", "suggested-action"] - Adw.ButtonContent { - icon-name: "plus-large-symbolic"; - label: _("New Snapshot"); - } - } - } - } - } + title: _("Snapshots List"); + Adw.ToastOverlay toast_overlay { + Adw.ToolbarView toolbar_view { + [top] + Adw.HeaderBar { + [start] + Button open_button { + icon-name: "folder-open-symbolic"; + tooltip-text: _("Open Snapshots Folder for this App"); + } + } + ScrolledWindow { + Adw.Clamp { + margin-start: 12; + margin-end: 12; + margin-top: 12; + margin-bottom: 12; + ListBox listbox { + valign: start; + selection-mode: none; + styles ["boxed-list"] + Adw.PreferencesGroup { + Adw.ActionRow {title: "test";} + } + } + } + } + [bottom] + ActionBar { + [center] + Button new_button { + margin-top: 3; + margin-bottom: 3; + styles ["pill", "suggested-action"] + Adw.ButtonContent { + icon-name: "plus-large-symbolic"; + label: _("New Snapshot"); + } + } + } + } + } } diff --git a/src/snapshot_page/snapshots_list_page.py b/src/snapshot_page/snapshots_list_page.py index 1c98549..f96eca7 100644 --- a/src/snapshot_page/snapshots_list_page.py +++ b/src/snapshot_page/snapshots_list_page.py @@ -8,103 +8,103 @@ import os @Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/snapshots_list_page.ui") 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: - self.set_title(package_or_folder) - folder = package_or_folder - self.new_button.set_sensitive(False) - self.new_button.set_tooltip_text(_("App not Installed")) - else: - folder = package_or_folder.info["id"] - self.set_title(_("{} Snapshots").format(package_or_folder.info["name"])) - if os.path.exists(package_or_folder.data_path): - self.new_button.set_sensitive(True) - self.new_button.set_tooltip_text(None) - 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) + __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: + self.set_title(package_or_folder) + folder = package_or_folder + self.new_button.set_sensitive(False) + self.new_button.set_tooltip_text(_("App not Installed")) + else: + folder = package_or_folder.info["id"] + self.set_title(_("{} Snapshots").format(package_or_folder.info["name"])) + if os.path.exists(package_or_folder.data_path): + self.new_button.set_sensitive(True) + self.new_button.set_tooltip_text(None) + 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) diff --git a/src/snapshot_page/tar_worker.py b/src/snapshot_page/tar_worker.py index d230112..993fdcf 100644 --- a/src/snapshot_page/tar_worker.py +++ b/src/snapshot_page/tar_worker.py @@ -4,116 +4,116 @@ from .error_toast import ErrorToast import os, subprocess, json class TarWorker: - def compress_thread(self, *args): - 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, - 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, - 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 - self.file_name = file_name - self.name = name - self.should_check = False - self.stop = False - self.fraction = 0.0 - self.total = 0 - self.process = None - self.toast_overlay = toast_overlay - self.has_cancelled = False - self.files_to_trash_on_cancel = [] + def compress_thread(self, *args): + 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, + 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, + 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 + self.file_name = file_name + self.name = name + self.should_check = False + self.stop = False + self.fraction = 0.0 + self.total = 0 + self.process = None + self.toast_overlay = toast_overlay + self.has_cancelled = False + self.files_to_trash_on_cancel = [] diff --git a/src/user_data_page/data_box.blp b/src/user_data_page/data_box.blp index 98e5686..c626bc8 100644 --- a/src/user_data_page/data_box.blp +++ b/src/user_data_page/data_box.blp @@ -2,83 +2,83 @@ using Gtk 4.0; using Adw 1; template $DataBox : ListBox { - selection-mode: none; - styles ["boxed-list"] - Adw.ActionRow row { - activatable: bind check_button.visible; - width-request: 275; - [child] - Box root_box { - orientation: vertical; - Box title_box { - margin-top: 12; - margin-bottom: 12; - Image image { - margin-start: 12; - margin-end: 12; - icon-name: "flatpak-symbolic"; - icon-size: large; - } - Box label_box { - orientation: vertical; - Label title_label { - label: "No Title Set"; - hexpand: true; - halign: start; - ellipsize: middle; - margin-end: 12; - styles ["title-4"] - } - Label subtitle_label { - label: "No subtitle set"; - // hexpand: true; - halign: start; - ellipsize: middle; - margin-end: 12; - } - } - } - Box content_box { - spacing: 6; - margin-start: 12; - margin-end: 6; - margin-bottom: 6; - Spinner spinner { - spinning: true; - } - Label size_label { - label: "No size set"; - halign: start; - hexpand: true; - } - Button copy_button { - icon-name: "copy-symbolic"; - tooltip-text: _("Copy Path"); - visible: bind check_button.visible inverted; - styles ["flat", "circular"] - } - Button open_button { - icon-name: "folder-open-symbolic"; - tooltip-text: _("Open User Data"); - visible: bind check_button.visible inverted; - styles ["flat", "circular"] - } - Button install_button { - icon-name: "arrow-pointing-at-line-down-symbolic"; - tooltip-text: _("Attempt to Install"); - styles ["flat", "circular"] - } - Button trash_button { - icon-name: "user-trash-symbolic"; - tooltip-text: _("Trash User Data"); - visible: bind check_button.visible inverted; - styles ["flat", "circular"] - } - CheckButton check_button { - visible: false; - styles ["selection-mode"] - } - } - } - } + selection-mode: none; + styles ["boxed-list"] + Adw.ActionRow row { + activatable: bind check_button.visible; + width-request: 275; + [child] + Box root_box { + orientation: vertical; + Box title_box { + margin-top: 12; + margin-bottom: 12; + Image image { + margin-start: 12; + margin-end: 12; + icon-name: "flatpak-symbolic"; + icon-size: large; + } + Box label_box { + orientation: vertical; + Label title_label { + label: "No Title Set"; + hexpand: true; + halign: start; + ellipsize: middle; + margin-end: 12; + styles ["title-4"] + } + Label subtitle_label { + label: "No subtitle set"; + // hexpand: true; + halign: start; + ellipsize: middle; + margin-end: 12; + } + } + } + Box content_box { + spacing: 6; + margin-start: 12; + margin-end: 6; + margin-bottom: 6; + Spinner spinner { + spinning: true; + } + Label size_label { + label: "No size set"; + halign: start; + hexpand: true; + } + Button copy_button { + icon-name: "copy-symbolic"; + tooltip-text: _("Copy Path"); + visible: bind check_button.visible inverted; + styles ["flat", "circular"] + } + Button open_button { + icon-name: "folder-open-symbolic"; + tooltip-text: _("Open User Data"); + visible: bind check_button.visible inverted; + styles ["flat", "circular"] + } + Button install_button { + icon-name: "arrow-pointing-at-line-down-symbolic"; + tooltip-text: _("Attempt to Install"); + styles ["flat", "circular"] + } + Button trash_button { + icon-name: "user-trash-symbolic"; + tooltip-text: _("Trash User Data"); + visible: bind check_button.visible inverted; + styles ["flat", "circular"] + } + CheckButton check_button { + visible: false; + styles ["selection-mode"] + } + } + } + } } diff --git a/src/user_data_page/data_box.py b/src/user_data_page/data_box.py index 4729630..b10f84a 100644 --- a/src/user_data_page/data_box.py +++ b/src/user_data_page/data_box.py @@ -6,143 +6,143 @@ import subprocess @Gtk.Template(resource_path="/io/github/flattool/Warehouse/user_data_page/data_box.ui") 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'] - # size *= 1024 - for unit in units: - if working_size < 1024: - 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) - self.install_button.set_visible(self.is_leftover) - 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) - properties_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].properties_page - 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) - else: - 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")) - dialog.add_response("continue", _("Continue")) - 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 - self.is_leftover = is_leftover - self.title = title - self.subtitle = subtitle - self.icon_path = icon_path - self.data_path = data_path - self.callback = callback - self.trash_callback = trash_callback - self.size = None - self.failed_trash = None - - # Apply - self.idle_stuff() - self.show_size() - if subtitle == "io.github.flattool.Warehouse": - 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) - self.install_button.connect("clicked", self.install_handler) - self.trash_button.connect("clicked", self.trash_handler) + __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'] + # size *= 1024 + for unit in units: + if working_size < 1024: + 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) + self.install_button.set_visible(self.is_leftover) + 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) + properties_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].properties_page + 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) + else: + 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")) + dialog.add_response("continue", _("Continue")) + 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 + self.is_leftover = is_leftover + self.title = title + self.subtitle = subtitle + self.icon_path = icon_path + self.data_path = data_path + self.callback = callback + self.trash_callback = trash_callback + self.size = None + self.failed_trash = None + + # Apply + self.idle_stuff() + self.show_size() + if subtitle == "io.github.flattool.Warehouse": + 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) + self.install_button.connect("clicked", self.install_handler) + self.trash_button.connect("clicked", self.trash_handler) diff --git a/src/user_data_page/data_subpage.blp b/src/user_data_page/data_subpage.blp index 965c49a..2b3af79 100644 --- a/src/user_data_page/data_subpage.blp +++ b/src/user_data_page/data_subpage.blp @@ -2,72 +2,72 @@ using Gtk 4.0; using Adw 1; template $DataSubpage : Stack { - Box content_box { - orientation: vertical; - Box label_box { - margin-start: 24; - margin-end: 24; - halign: fill; - hexpand: true; - Label title { - label: _("No Title Set"); - styles ["title-1"] - hexpand: true; - justify: fill; - halign: start; - wrap: true; - } - Box subtitle_size_box { - Spinner spinner { - spinning: true; - valign: center; - margin-top: 3; - margin-end: 6; - } - Label size_label { - label: _("Loading Size…"); - styles ["title-3"] - halign: start; - wrap: true; - } - Label subtitle { - visible: false; - label: "No Subtutle Set"; - styles ["title-3"] - wrap: true; - } - } - margin-bottom: 9; - } - ScrolledWindow scrolled_window { - vexpand: true; - Box { - orientation: vertical; - Separator { - margin-start: 12; - margin-end: 12; - margin-bottom: 9; - } - FlowBox flow_box { - styles ["boxed-list"] - homogeneous: true; - valign: start; - selection-mode: none; - max-children-per-line: 6; - margin-start: 12; - margin-end: 12; - margin-bottom: 12; - } - } - } - } - Adw.StatusPage no_data { - // Contents will be set from the subpage object - } - Adw.StatusPage no_results { - title: _("No Results Found"); - description: _("Try a different search"); - icon-name: "system-search-symbolic"; - valign: center; - } + Box content_box { + orientation: vertical; + Box label_box { + margin-start: 24; + margin-end: 24; + halign: fill; + hexpand: true; + Label title { + label: _("No Title Set"); + styles ["title-1"] + hexpand: true; + justify: fill; + halign: start; + wrap: true; + } + Box subtitle_size_box { + Spinner spinner { + spinning: true; + valign: center; + margin-top: 3; + margin-end: 6; + } + Label size_label { + label: _("Loading Size…"); + styles ["title-3"] + halign: start; + wrap: true; + } + Label subtitle { + visible: false; + label: "No Subtutle Set"; + styles ["title-3"] + wrap: true; + } + } + margin-bottom: 9; + } + ScrolledWindow scrolled_window { + vexpand: true; + Box { + orientation: vertical; + Separator { + margin-start: 12; + margin-end: 12; + margin-bottom: 9; + } + FlowBox flow_box { + styles ["boxed-list"] + homogeneous: true; + valign: start; + selection-mode: none; + max-children-per-line: 6; + margin-start: 12; + margin-end: 12; + margin-bottom: 12; + } + } + } + } + Adw.StatusPage no_data { + // Contents will be set from the subpage object + } + Adw.StatusPage no_results { + title: _("No Results Found"); + description: _("Try a different search"); + icon-name: "system-search-symbolic"; + valign: center; + } } diff --git a/src/user_data_page/data_subpage.py b/src/user_data_page/data_subpage.py index 1034075..78ab046 100644 --- a/src/user_data_page/data_subpage.py +++ b/src/user_data_page/data_subpage.py @@ -5,237 +5,237 @@ from .loading_status import LoadingStatus @Gtk.Template(resource_path="/io/github/flattool/Warehouse/user_data_page/data_subpage.ui") class DataSubpage(Gtk.Stack): - __gtype_name__ = 'DataSubpage' - gtc = Gtk.Template.Child + __gtype_name__ = 'DataSubpage' + gtc = Gtk.Template.Child - scrolled_window = gtc() - - label_box = gtc() - subtitle_size_box = gtc() - title = gtc() - subtitle = gtc() - spinner = gtc() - size_label = gtc() - flow_box = gtc() + scrolled_window = gtc() + + label_box = gtc() + subtitle_size_box = gtc() + title = gtc() + subtitle = gtc() + spinner = gtc() + size_label = gtc() + flow_box = gtc() - # Statuses - content_box = gtc() - no_data = gtc() - no_results = gtc() + # Statuses + content_box = gtc() + no_data = gtc() + no_results = gtc() - def human_readable_size(self): - working_size = self.total_size - units = ['KB', 'MB', 'GB', 'TB'] - # size *= 1024 - for unit in units: - if working_size < 1024: - return f"~ {round(working_size)} {unit}" - working_size /= 1024 - return f"~ {round(working_size)} PB" + def human_readable_size(self): + working_size = self.total_size + units = ['KB', 'MB', 'GB', 'TB'] + # size *= 1024 + for unit in units: + if working_size < 1024: + return f"~ {round(working_size)} {unit}" + working_size /= 1024 + return f"~ {round(working_size)} PB" - def sort_func(self, box1, box2): - import random - # print(random.randint(1, 100), self.sort_mode, self.sort_ascend) - i1 = None - i2 = None - if self.sort_mode == "name": - i1 = box1.get_child().title.lower() - i2 = box2.get_child().title.lower() + def sort_func(self, box1, box2): + import random + # print(random.randint(1, 100), self.sort_mode, self.sort_ascend) + i1 = None + i2 = None + if self.sort_mode == "name": + i1 = box1.get_child().title.lower() + i2 = box2.get_child().title.lower() - if self.sort_mode == "id": - i1 = box1.get_child().subtitle.lower() - i2 = box2.get_child().subtitle.lower() + if self.sort_mode == "id": + i1 = box1.get_child().subtitle.lower() + i2 = box2.get_child().subtitle.lower() - if self.sort_mode == "size" and self.ready_to_sort_size: - i1 = box1.get_child().size - i2 = box2.get_child().size + if self.sort_mode == "size" and self.ready_to_sort_size: + i1 = box1.get_child().size + i2 = box2.get_child().size - if i1 is None or i2 is None: - return 0 + if i1 is None or i2 is None: + return 0 - return i1 > i2 if self.sort_ascend else i1 < i2 + return i1 > i2 if self.sort_ascend else i1 < i2 - def box_size_callback(self, size): - self.finished_boxes += 1 - self.total_size += size - if self.finished_boxes == self.total_items: - self.size_label.set_label(self.human_readable_size()) - self.spinner.set_visible(False) - self.ready_to_sort_size = True - if self.sort_mode == "size": - self.flow_box.invalidate_sort() - self.set_visible_child(self.content_box) - GLib.idle_add(lambda *_: self.parent_page.status_stack.set_visible_child(self.parent_page.main_view)) + def box_size_callback(self, size): + self.finished_boxes += 1 + self.total_size += size + if self.finished_boxes == self.total_items: + self.size_label.set_label(self.human_readable_size()) + self.spinner.set_visible(False) + self.ready_to_sort_size = True + if self.sort_mode == "size": + self.flow_box.invalidate_sort() + self.set_visible_child(self.content_box) + GLib.idle_add(lambda *_: self.parent_page.status_stack.set_visible_child(self.parent_page.main_view)) - def trash_handler(self, trashed_box): - self.flow_box.remove(trashed_box) - if not self.flow_box.get_child_at_index(0): - self.set_visible_child(self.no_data) - self.parent_page.start_loading() - self.parent_page.end_loading() + def trash_handler(self, trashed_box): + self.flow_box.remove(trashed_box) + if not self.flow_box.get_child_at_index(0): + self.set_visible_child(self.no_data) + self.parent_page.start_loading() + self.parent_page.end_loading() - def set_selection_mode(self, is_enabled): - if not is_enabled: - self.size_label.set_visible(True) - self.subtitle.set_visible(False) + def set_selection_mode(self, is_enabled): + if not is_enabled: + self.size_label.set_visible(True) + self.subtitle.set_visible(False) - idx = 0 - while box := self.flow_box.get_child_at_index(idx): - idx += 1 - box = box.get_child() - if not is_enabled: - GLib.idle_add(lambda *_, box=box: box.check_button.set_active(False)) + idx = 0 + while box := self.flow_box.get_child_at_index(idx): + idx += 1 + box = box.get_child() + if not is_enabled: + GLib.idle_add(lambda *_, box=box: box.check_button.set_active(False)) - GLib.idle_add(lambda *_, box=box: box.check_button.set_visible(is_enabled)) - GLib.idle_add(lambda *_, box=box: box.install_button.set_visible(box.is_leftover and not is_enabled)) + GLib.idle_add(lambda *_, box=box: box.check_button.set_visible(is_enabled)) + GLib.idle_add(lambda *_, box=box: box.install_button.set_visible(box.is_leftover and not is_enabled)) - self.selected_boxes.clear() + self.selected_boxes.clear() - def box_select_handler(self, box): - cb = box.check_button - if cb.get_active(): - self.selected_boxes.append(box) - else: - try: - 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) - self.subtitle.set_label(_("{} Selected").format(total)) - self.parent_page.copy_button.set_sensitive(total) - self.parent_page.trash_button.set_sensitive(total) - self.parent_page.install_button.set_sensitive(total) - self.parent_page.more_button.set_sensitive(total) + def box_select_handler(self, box): + cb = box.check_button + if cb.get_active(): + self.selected_boxes.append(box) + else: + try: + 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) + self.subtitle.set_label(_("{} Selected").format(total)) + self.parent_page.copy_button.set_sensitive(total) + self.parent_page.trash_button.set_sensitive(total) + self.parent_page.install_button.set_sensitive(total) + self.parent_page.more_button.set_sensitive(total) - def box_interact_handler(self, flow_box, box): - box = box.get_child() - cb = box.check_button - if cb.get_visible(): - cb.set_active(not cb.get_active()) + def box_interact_handler(self, flow_box, box): + box = box.get_child() + cb = box.check_button + if cb.get_visible(): + cb.set_active(not cb.get_active()) - def select_all_handler(self, *args): - idx = 0 - while box := self.flow_box.get_child_at_index(idx): - idx += 1 - box.get_child().check_button.set_active(True) + def select_all_handler(self, *args): + idx = 0 + while box := self.flow_box.get_child_at_index(idx): + idx += 1 + box.get_child().check_button.set_active(True) - def box_rclick_handler(self, box): - if self.should_rclick: - self.parent_page.select_button.set_active(True) - box.check_button.set_active(not box.check_button.get_active()) + def box_rclick_handler(self, box): + if self.should_rclick: + self.parent_page.select_button.set_active(True) + box.check_button.set_active(not box.check_button.get_active()) - def generate_list(self, flatpaks, data): - self.flow_box.remove_all() - self.boxes.clear() - self.ready_to_sort_size = False - self.finished_boxes = 0 - self.total_size = 0 - self.total_items = len(data) - self.parent_page.search_entry.set_editable(True) - self.should_rclick = True - if flatpaks: - for i, pak in enumerate(flatpaks): - box = DataBox(self, self.parent_page.toast_overlay, False, pak.info["name"], pak.info["id"], pak.data_path, pak.icon_path, self.box_size_callback, self.trash_handler) - box.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box)) - self.boxes.append(box) - self.flow_box.append(box) + def generate_list(self, flatpaks, data): + self.flow_box.remove_all() + self.boxes.clear() + self.ready_to_sort_size = False + self.finished_boxes = 0 + self.total_size = 0 + self.total_items = len(data) + self.parent_page.search_entry.set_editable(True) + self.should_rclick = True + if flatpaks: + for i, pak in enumerate(flatpaks): + box = DataBox(self, self.parent_page.toast_overlay, False, pak.info["name"], pak.info["id"], pak.data_path, pak.icon_path, self.box_size_callback, self.trash_handler) + box.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box)) + self.boxes.append(box) + self.flow_box.append(box) - else: - for i, folder in enumerate(data): - 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 - box.set_focusable(False) - child = box.get_child() - child.set_focusable(False) - child.row.set_focusable(child.check_button.get_visible()) - rclick = Gtk.GestureClick(button=3) - rclick.connect("released", lambda *_, child=child: self.box_rclick_handler(child)) - box.add_controller(rclick) - long_press = Gtk.GestureLongPress() - long_press.connect("pressed", lambda *_, child=child: self.box_rclick_handler(child)) - box.add_controller(long_press) + else: + for i, folder in enumerate(data): + 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 + box.set_focusable(False) + child = box.get_child() + child.set_focusable(False) + child.row.set_focusable(child.check_button.get_visible()) + rclick = Gtk.GestureClick(button=3) + rclick.connect("released", lambda *_, child=child: self.box_rclick_handler(child)) + box.add_controller(rclick) + long_press = Gtk.GestureLongPress() + long_press.connect("pressed", lambda *_, child=child: self.box_rclick_handler(child)) + box.add_controller(long_press) - if idx == 0: - self.set_visible_child(self.no_data) - elif self.sort_mode != "size": - self.set_visible_child(self.content_box) - self.parent_page.status_stack.set_visible_child(self.parent_page.main_view) + if idx == 0: + self.set_visible_child(self.no_data) + elif self.sort_mode != "size": + self.set_visible_child(self.content_box) + self.parent_page.status_stack.set_visible_child(self.parent_page.main_view) - def filter_func(self, box): - search_text = self.parent_page.search_entry.get_text().lower() - box = box.get_child() - if search_text in box.title.lower() or search_text in box.subtitle.lower(): - self.is_result = True - return True + def filter_func(self, box): + search_text = self.parent_page.search_entry.get_text().lower() + box = box.get_child() + if search_text in box.title.lower() or search_text in box.subtitle.lower(): + self.is_result = True + return True - def on_invalidate(self, box): - current_status = self.get_visible_child() - if not current_status is self.no_results: - self.prev_status = self.get_visible_child() + def on_invalidate(self, box): + current_status = self.get_visible_child() + if not current_status is self.no_results: + self.prev_status = self.get_visible_child() - self.is_result = False - self.flow_box.invalidate_filter() - if self.is_result: - self.set_visible_child(self.prev_status) - else: - self.set_visible_child(self.no_results) + self.is_result = False + self.flow_box.invalidate_filter() + if self.is_result: + self.set_visible_child(self.prev_status) + else: + self.set_visible_child(self.no_results) - if self.parent_page.search_entry.get_text().lower() != "" and self.total_items == 0: - self.set_visible_child(self.no_results) - elif self.total_items == 0: - self.set_visible_child(self.no_data) + if self.parent_page.search_entry.get_text().lower() != "" and self.total_items == 0: + self.set_visible_child(self.no_results) + elif self.total_items == 0: + self.set_visible_child(self.no_data) - def update_sort_mode(self): - self.sort_ascend = self.settings.get_boolean("sort-ascend") - self.sort_mode = self.settings.get_string("sort-mode") - self.flow_box.invalidate_sort() + def update_sort_mode(self): + self.sort_ascend = self.settings.get_boolean("sort-ascend") + self.sort_mode = self.settings.get_string("sort-mode") + self.flow_box.invalidate_sort() - def __init__(self, title, parent_page, is_active, main_window, **kwargs): - super().__init__(**kwargs) + def __init__(self, title, parent_page, is_active, main_window, **kwargs): + super().__init__(**kwargs) - GLib.idle_add(lambda *_: self.title.set_label(title)) + GLib.idle_add(lambda *_: self.title.set_label(title)) - # self.select_button.connect("toggled", lambda *_: self.set_selection_mode(self.select_button.get_active())) - # self.flow_box.connect("child-activated", lambda _, item: (cb := (row := item.get_child()).check_button).set_active((not cb.get_active()) if row.get_activatable() else False)) + # self.select_button.connect("toggled", lambda *_: self.set_selection_mode(self.select_button.get_active())) + # self.flow_box.connect("child-activated", lambda _, item: (cb := (row := item.get_child()).check_button).set_active((not cb.get_active()) if row.get_activatable() else False)) - # Extra Object Creation - self.main_window = main_window - self.parent_page = parent_page - # self.is_active = is_active - self.sort_mode = "" - self.sort_ascend = False - self.total_size = 0 - self.total_items = 0 - self.boxes = [] - self.selected_boxes = [] - self.ready_to_sort_size = False - self.should_rclick = True - self.finished_boxes = 0 - self.is_result = False - self.prev_status = None - self.settings = Gio.Settings.new("io.github.flattool.Warehouse.data_page") + # Extra Object Creation + self.main_window = main_window + self.parent_page = parent_page + # self.is_active = is_active + self.sort_mode = "" + self.sort_ascend = False + self.total_size = 0 + self.total_items = 0 + self.boxes = [] + self.selected_boxes = [] + self.ready_to_sort_size = False + self.should_rclick = True + self.finished_boxes = 0 + self.is_result = False + self.prev_status = None + self.settings = Gio.Settings.new("io.github.flattool.Warehouse.data_page") - # Apply - self.flow_box.set_sort_func(self.sort_func) - self.flow_box.set_filter_func(self.filter_func) + # Apply + self.flow_box.set_sort_func(self.sort_func) + self.flow_box.set_filter_func(self.filter_func) - if is_active: - self.no_data.set_icon_name("error-symbolic") - self.no_data.set_title(_("No Active Data")) - self.no_data.set_description(_("Warehouse cannot see any active user data or your system has no active user data present")) - else: - self.no_data.set_icon_name("check-plain-symbolic") - self.no_data.set_title(_("No Leftover Data")) - self.no_data.set_description(_("There is no leftover user data")) + if is_active: + self.no_data.set_icon_name("error-symbolic") + self.no_data.set_title(_("No Active Data")) + self.no_data.set_description(_("Warehouse cannot see any active user data or your system has no active user data present")) + else: + self.no_data.set_icon_name("check-plain-symbolic") + self.no_data.set_title(_("No Leftover Data")) + self.no_data.set_description(_("There is no leftover user data")) - # Connections - parent_page.search_entry.connect("search-changed", self.on_invalidate) - self.flow_box.connect("child-activated", self.box_interact_handler) + # Connections + parent_page.search_entry.connect("search-changed", self.on_invalidate) + self.flow_box.connect("child-activated", self.box_interact_handler) diff --git a/src/user_data_page/user_data_page.blp b/src/user_data_page/user_data_page.blp index 4cbabf5..69cd21c 100644 --- a/src/user_data_page/user_data_page.blp +++ b/src/user_data_page/user_data_page.blp @@ -2,210 +2,210 @@ using Gtk 4.0; using Adw 1; template $UserDataPage : Adw.BreakpointBin { - width-request: 1; - height-request: 1; + width-request: 1; + height-request: 1; - Adw.Breakpoint bpt { - condition ("max-width: 585") + Adw.Breakpoint bpt { + condition ("max-width: 585") - setters { - header_bar.title-widget: null; - // header_bar.show-title: false; - switcher_bar.reveal: true; - switcher_bar.visible: true; - } - } + setters { + header_bar.title-widget: null; + // header_bar.show-title: false; + switcher_bar.reveal: true; + switcher_bar.visible: true; + } + } - Adw.NavigationPage { - title: _("User Data"); - Stack status_stack { - Adw.ToolbarView loading_view { - [top] - Adw.HeaderBar { - [start] - $SidebarButton {} - } - } - Adw.ToolbarView main_view { - [top] - Adw.HeaderBar header_bar { - title-widget: - Adw.ViewSwitcher { - stack: stack; - policy: wide; - } - ; - [start] - $SidebarButton {} - [start] - ToggleButton search_button { - icon-name: "system-search-symbolic"; - tooltip-text: _("Search User Data"); - } - [end] - MenuButton sort_button { - popover: sort_pop; - icon-name: "vertical-arrows-long-symbolic"; - tooltip-text: _("Sort User Data"); - } - [end] - ToggleButton select_button { - icon-name: "selection-mode-symbolic"; - tooltip-text: _("Select User Data"); - } - } - [top] - Adw.Clamp { - SearchBar search_bar { - search-mode-enabled: bind search_button.active bidirectional; - SearchEntry search_entry { - editable: false; - hexpand: true; - placeholder-text: _("Search User Data"); - } - } - } - [bottom] - Revealer revealer { - reveal-child: bind select_button.active; - transition-type: slide_up; - [center] - Box bottom_bar { - styles ["toolbar"] - hexpand: true; - homogeneous: true; - Button select_all_button { - styles ["raised"] - Adw.ButtonContent { - icon-name: "selection-mode-symbolic"; - label: _("Select All"); - can-shrink: true; - } - } - Button copy_button { - sensitive: false; - styles ["raised"] - Adw.ButtonContent { - icon-name: "edit-copy-symbolic"; - label: _("Copy"); - can-shrink: true; - } - } - Button install_button { - visible: false; - sensitive: false; - styles ["raised"] - Adw.ButtonContent { - icon-name: "arrow-pointing-at-line-down-symbolic"; - label: _("Install"); - can-shrink: true; - } - } - Button trash_button { - sensitive: false; - styles ["raised"] - Adw.ButtonContent { - icon-name: "user-trash-symbolic"; - label: _("Trash"); - can-shrink: true; - } - } - MenuButton more_button { - visible: false; - sensitive: false; - popover: more_popover; - styles ["raised"] - Adw.ButtonContent { - icon-name: "view-more-symbolic"; - label: _("More"); - can-shrink: true; - } - } - } - } - [bottom] - Adw.ViewSwitcherBar switcher_bar { - stack: stack; - visible: false; - } - Adw.ToastOverlay toast_overlay { - Adw.ViewStack stack { - } - } - } - } - } + Adw.NavigationPage { + title: _("User Data"); + Stack status_stack { + Adw.ToolbarView loading_view { + [top] + Adw.HeaderBar { + [start] + $SidebarButton {} + } + } + Adw.ToolbarView main_view { + [top] + Adw.HeaderBar header_bar { + title-widget: + Adw.ViewSwitcher { + stack: stack; + policy: wide; + } + ; + [start] + $SidebarButton {} + [start] + ToggleButton search_button { + icon-name: "system-search-symbolic"; + tooltip-text: _("Search User Data"); + } + [end] + MenuButton sort_button { + popover: sort_pop; + icon-name: "vertical-arrows-long-symbolic"; + tooltip-text: _("Sort User Data"); + } + [end] + ToggleButton select_button { + icon-name: "selection-mode-symbolic"; + tooltip-text: _("Select User Data"); + } + } + [top] + Adw.Clamp { + SearchBar search_bar { + search-mode-enabled: bind search_button.active bidirectional; + SearchEntry search_entry { + editable: false; + hexpand: true; + placeholder-text: _("Search User Data"); + } + } + } + [bottom] + Revealer revealer { + reveal-child: bind select_button.active; + transition-type: slide_up; + [center] + Box bottom_bar { + styles ["toolbar"] + hexpand: true; + homogeneous: true; + Button select_all_button { + styles ["raised"] + Adw.ButtonContent { + icon-name: "selection-mode-symbolic"; + label: _("Select All"); + can-shrink: true; + } + } + Button copy_button { + sensitive: false; + styles ["raised"] + Adw.ButtonContent { + icon-name: "edit-copy-symbolic"; + label: _("Copy"); + can-shrink: true; + } + } + Button install_button { + visible: false; + sensitive: false; + styles ["raised"] + Adw.ButtonContent { + icon-name: "arrow-pointing-at-line-down-symbolic"; + label: _("Install"); + can-shrink: true; + } + } + Button trash_button { + sensitive: false; + styles ["raised"] + Adw.ButtonContent { + icon-name: "user-trash-symbolic"; + label: _("Trash"); + can-shrink: true; + } + } + MenuButton more_button { + visible: false; + sensitive: false; + popover: more_popover; + styles ["raised"] + Adw.ButtonContent { + icon-name: "view-more-symbolic"; + label: _("More"); + can-shrink: true; + } + } + } + } + [bottom] + Adw.ViewSwitcherBar switcher_bar { + stack: stack; + visible: false; + } + Adw.ToastOverlay toast_overlay { + Adw.ViewStack stack { + } + } + } + } + } } Popover more_popover { - styles ["menu"] - ListBox more_menu { - Label more_install { - label: _("Install"); - halign: start; - } - Label more_trash { - label: _("Trash"); - halign: start; - } - } + styles ["menu"] + ListBox more_menu { + Label more_install { + label: _("Install"); + halign: start; + } + Label more_trash { + label: _("Trash"); + halign: start; + } + } } Popover sort_pop { - styles ["menu"] - Box { - orientation: vertical; - margin-start: 6; - margin-end: 6; - margin-top: 6; - margin-bottom: 6; - Box { - homogeneous: true; - spacing: 3; - ToggleButton sort_ascend { - styles ["flat"] - Adw.ButtonContent { - icon-name: "view-sort-ascending-symbolic"; - label: _("Ascending"); - } - } - ToggleButton sort_descend { - group: sort_ascend; - styles ["flat"] - Adw.ButtonContent { - icon-name: "view-sort-descending-symbolic"; - label: _("Descending"); - } - } - } - Separator { - } - Box { - homogeneous: true; - spacing: 3; - ToggleButton sort_name { - styles ["flat"] - Adw.ButtonContent { - icon-name: "font-x-generic-symbolic"; - label: _("Name"); - } - } - ToggleButton sort_id { - group: sort_name; - styles ["flat"] - Adw.ButtonContent { - icon-name: "tag-outline-symbolic"; - label: _("ID"); - } - } - ToggleButton sort_size { - group: sort_name; - styles ["flat"] - Adw.ButtonContent { - icon-name: "harddisk-symbolic"; - label: _("Size"); - } - } - } - } + styles ["menu"] + Box { + orientation: vertical; + margin-start: 6; + margin-end: 6; + margin-top: 6; + margin-bottom: 6; + Box { + homogeneous: true; + spacing: 3; + ToggleButton sort_ascend { + styles ["flat"] + Adw.ButtonContent { + icon-name: "view-sort-ascending-symbolic"; + label: _("Ascending"); + } + } + ToggleButton sort_descend { + group: sort_ascend; + styles ["flat"] + Adw.ButtonContent { + icon-name: "view-sort-descending-symbolic"; + label: _("Descending"); + } + } + } + Separator { + } + Box { + homogeneous: true; + spacing: 3; + ToggleButton sort_name { + styles ["flat"] + Adw.ButtonContent { + icon-name: "font-x-generic-symbolic"; + label: _("Name"); + } + } + ToggleButton sort_id { + group: sort_name; + styles ["flat"] + Adw.ButtonContent { + icon-name: "tag-outline-symbolic"; + label: _("ID"); + } + } + ToggleButton sort_size { + group: sort_name; + styles ["flat"] + Adw.ButtonContent { + icon-name: "harddisk-symbolic"; + label: _("Size"); + } + } + } + } } diff --git a/src/user_data_page/user_data_page.py b/src/user_data_page/user_data_page.py index 3ad49bb..4f50333 100644 --- a/src/user_data_page/user_data_page.py +++ b/src/user_data_page/user_data_page.py @@ -9,290 +9,290 @@ import os, subprocess @Gtk.Template(resource_path="/io/github/flattool/Warehouse/user_data_page/user_data_page.ui") class UserDataPage(Adw.BreakpointBin): - __gtype_name__ = 'UserDataPage' - gtc = Gtk.Template.Child - - bpt = gtc() - status_stack = gtc() - loading_view = gtc() - main_view = gtc() - header_bar = gtc() - switcher_bar = gtc() - search_button = gtc() - select_button = gtc() - sort_button = gtc() - search_bar = gtc() - search_entry = gtc() - 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() - install_button = gtc() - more_button = gtc() - more_popover = gtc() - 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 - instance = None - page_name = "user-data" - 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) - self.select_button.set_active(False) - self.adp.size_label.set_label(_("Loading Size")) - 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") - self.sort_modes_to_buttons[mode].set_active(True) - (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: - self.search_button.set_active(False) - self.search_button.set_sensitive(False) - self.select_button.set_active(False) - self.select_button.set_sensitive(False) - self.sort_button.set_active(False) - self.sort_button.set_sensitive(False) - self.search_entry.set_editable(False) - else: - self.search_button.set_sensitive(True) - 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) - self.ldp.set_selection_mode(active) - if not active: - self.copy_button.set_sensitive(False) - 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: - subprocess.run(cmd, check=True, capture_output=True, text=True) - properties_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].properties_page - 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() - if error[0]: - 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")) - dialog.add_response("continue", _("Continue")) - 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) - self.ldp.label_box.set_orientation(Gtk.Orientation.VERTICAL if is_applied else Gtk.Orientation.HORIZONTAL) - child = self.stack.get_visible_child() - 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() - match row: - case self.more_install: - 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) - self.ldp = DataSubpage(_("Leftover Data"), self, False, main_window) - self.data_flatpaks = [] - self.active_data = [] - self.leftover_data = [] - self.total_items = 0 - self.settings = Gio.Settings.new("io.github.flattool.Warehouse.data_page") - self.sort_modes_to_buttons = { - "name": self.sort_name, - "id": self.sort_id, - "size": self.sort_size, - } - 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", - title=_("Active Data"), - icon_name="file-manager-symbolic", - ) - self.stack.add_titled_with_icon( - child=self.ldp, - name="leftover", - 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) - self.select_all_button.connect("clicked", self.select_all_handler) - self.copy_button.connect("clicked", self.copy_handler) - self.trash_button.connect("clicked", self.selection_trash_handler) - self.install_button.connect("clicked", self.install_handler) - self.more_menu.connect("row-activated", self.more_menu_handler) - self.sort_ascend.connect("clicked", self.sort_button_handler) - self.sort_descend.connect("clicked", self.sort_button_handler) - self.sort_name.connect("clicked", self.sort_button_handler) - self.sort_id.connect("clicked", self.sort_button_handler) - self.sort_size.connect("clicked", self.sort_button_handler) - self.bpt.connect("apply", self.breakpoint_handler, True) - self.bpt.connect("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) - self.load_sort_settings() + __gtype_name__ = 'UserDataPage' + gtc = Gtk.Template.Child + + bpt = gtc() + status_stack = gtc() + loading_view = gtc() + main_view = gtc() + header_bar = gtc() + switcher_bar = gtc() + search_button = gtc() + select_button = gtc() + sort_button = gtc() + search_bar = gtc() + search_entry = gtc() + 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() + install_button = gtc() + more_button = gtc() + more_popover = gtc() + 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 + instance = None + page_name = "user-data" + 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) + self.select_button.set_active(False) + self.adp.size_label.set_label(_("Loading Size")) + 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") + self.sort_modes_to_buttons[mode].set_active(True) + (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: + self.search_button.set_active(False) + self.search_button.set_sensitive(False) + self.select_button.set_active(False) + self.select_button.set_sensitive(False) + self.sort_button.set_active(False) + self.sort_button.set_sensitive(False) + self.search_entry.set_editable(False) + else: + self.search_button.set_sensitive(True) + 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) + self.ldp.set_selection_mode(active) + if not active: + self.copy_button.set_sensitive(False) + 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: + subprocess.run(cmd, check=True, capture_output=True, text=True) + properties_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].properties_page + 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() + if error[0]: + 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")) + dialog.add_response("continue", _("Continue")) + 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) + self.ldp.label_box.set_orientation(Gtk.Orientation.VERTICAL if is_applied else Gtk.Orientation.HORIZONTAL) + child = self.stack.get_visible_child() + 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() + match row: + case self.more_install: + 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) + self.ldp = DataSubpage(_("Leftover Data"), self, False, main_window) + self.data_flatpaks = [] + self.active_data = [] + self.leftover_data = [] + self.total_items = 0 + self.settings = Gio.Settings.new("io.github.flattool.Warehouse.data_page") + self.sort_modes_to_buttons = { + "name": self.sort_name, + "id": self.sort_id, + "size": self.sort_size, + } + 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", + title=_("Active Data"), + icon_name="file-manager-symbolic", + ) + self.stack.add_titled_with_icon( + child=self.ldp, + name="leftover", + 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) + self.select_all_button.connect("clicked", self.select_all_handler) + self.copy_button.connect("clicked", self.copy_handler) + self.trash_button.connect("clicked", self.selection_trash_handler) + self.install_button.connect("clicked", self.install_handler) + self.more_menu.connect("row-activated", self.more_menu_handler) + self.sort_ascend.connect("clicked", self.sort_button_handler) + self.sort_descend.connect("clicked", self.sort_button_handler) + self.sort_name.connect("clicked", self.sort_button_handler) + self.sort_id.connect("clicked", self.sort_button_handler) + self.sort_size.connect("clicked", self.sort_button_handler) + self.bpt.connect("apply", self.breakpoint_handler, True) + self.bpt.connect("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) + self.load_sort_settings() diff --git a/src/warehouse.gresource.xml b/src/warehouse.gresource.xml index 8102b6d..8e9c3e8 100644 --- a/src/warehouse.gresource.xml +++ b/src/warehouse.gresource.xml @@ -1,85 +1,85 @@ - ../data/style.css - gtk/help-overlay.ui - gtk/loading_status.ui - gtk/app_row.ui - gtk/installation_chooser.ui - gtk/attempt_install_dialog.ui - main_window/window.ui - packages_page/packages_page.ui - packages_page/filters_page.ui - packages_page/uninstall_dialog.ui - properties_page/properties_page.ui - change_version_page/change_version_page.ui - user_data_page/data_box.ui - user_data_page/user_data_page.ui - user_data_page/data_subpage.ui - remotes_page/remotes_page.ui - remotes_page/remote_row.ui - remotes_page/add_remote_dialog.ui - snapshot_page/snapshot_page.ui - snapshot_page/snapshots_list_page.ui - snapshot_page/snapshot_box.ui - snapshot_page/new_snapshot_dialog.ui - install_page/file_install_dialog.ui - install_page/install_page.ui - install_page/result_row.ui - install_page/select_page.ui - install_page/results_page.ui - install_page/pending_page.ui - + ../data/style.css + gtk/help-overlay.ui + gtk/loading_status.ui + gtk/app_row.ui + gtk/installation_chooser.ui + gtk/attempt_install_dialog.ui + main_window/window.ui + packages_page/packages_page.ui + packages_page/filters_page.ui + packages_page/uninstall_dialog.ui + properties_page/properties_page.ui + change_version_page/change_version_page.ui + user_data_page/data_box.ui + user_data_page/user_data_page.ui + user_data_page/data_subpage.ui + remotes_page/remotes_page.ui + remotes_page/remote_row.ui + remotes_page/add_remote_dialog.ui + snapshot_page/snapshot_page.ui + snapshot_page/snapshots_list_page.ui + snapshot_page/snapshot_box.ui + snapshot_page/new_snapshot_dialog.ui + install_page/file_install_dialog.ui + install_page/install_page.ui + install_page/result_row.ui + install_page/select_page.ui + install_page/results_page.ui + install_page/pending_page.ui + - ../data/icons/selection-mode-symbolic.svg - ../data/icons/error-symbolic.svg - ../data/icons/user-trash-symbolic.svg - ../data/icons/folder-visiting-symbolic.svg - ../data/icons/info-symbolic.svg - ../data/icons/check-plain-symbolic.svg - ../data/icons/paper-filled-symbolic.svg - ../data/icons/plus-large-symbolic.svg - ../data/icons/funnel-symbolic.svg - ../data/icons/flatpak-symbolic.svg - ../data/icons/right-large-symbolic.svg - ../data/icons/view-more-symbolic.svg - ../data/icons/clock-alt-symbolic.svg - ../data/icons/arrow2-top-right-symbolic.svg - ../data/icons/software-update-available-symbolic.svg - ../data/icons/software-update-urgent-symbolic.svg - ../data/icons/cross-filled-symbolic.svg - ../data/icons/important-small-symbolic.svg - ../data/icons/eye-not-looking-symbolic.svg - ../data/icons/eye-open-negative-filled-symbolic.svg - ../data/icons/left-large-symbolic.svg - ../data/icons/arrow-turn-left-down-symbolic.svg - ../data/icons/arrow-circular-top-right-symbolic.svg - ../data/icons/dock-left-symbolic.svg - ../data/icons/server-pick-symbolic.svg - ../data/icons/file-manager-symbolic.svg - ../data/icons/snapshots-alt-symbolic.svg - ../data/icons/arrow-pointing-at-line-down-symbolic.svg - ../data/icons/loupe-large-symbolic.svg - ../data/icons/folder-open-symbolic.svg - ../data/icons/padlock2-symbolic.svg - ../data/icons/pin-symbolic.svg - ../data/icons/pin-small-symbolic.svg - ../data/icons/error-small-symbolic.svg - ../data/icons/copy-symbolic.svg - ../data/icons/double-ended-arrows-vertical-symbolic.svg - ../data/icons/vertical-arrows-long-symbolic.svg - ../data/icons/dot-symbolic.svg - ../data/icons/folder-templates-symbolic.svg - ../data/icons/view-sort-ascending-symbolic.svg - ../data/icons/view-sort-descending-symbolic.svg - ../data/icons/font-x-generic-symbolic.svg - ../data/icons/tag-outline-symbolic.svg - ../data/icons/harddisk-symbolic.svg - ../data/icons/arrow-turn-down-right-symbolic.svg - ../data/icons/minus-large-symbolic.svg - ../data/icons/view-list-bullet-symbolic.svg - ../data/icons/list-remove-all-symbolic.svg - ../data/icons/edit-symbolic.svg + ../data/icons/selection-mode-symbolic.svg + ../data/icons/error-symbolic.svg + ../data/icons/user-trash-symbolic.svg + ../data/icons/folder-visiting-symbolic.svg + ../data/icons/info-symbolic.svg + ../data/icons/check-plain-symbolic.svg + ../data/icons/paper-filled-symbolic.svg + ../data/icons/plus-large-symbolic.svg + ../data/icons/funnel-symbolic.svg + ../data/icons/flatpak-symbolic.svg + ../data/icons/right-large-symbolic.svg + ../data/icons/view-more-symbolic.svg + ../data/icons/clock-alt-symbolic.svg + ../data/icons/arrow2-top-right-symbolic.svg + ../data/icons/software-update-available-symbolic.svg + ../data/icons/software-update-urgent-symbolic.svg + ../data/icons/cross-filled-symbolic.svg + ../data/icons/important-small-symbolic.svg + ../data/icons/eye-not-looking-symbolic.svg + ../data/icons/eye-open-negative-filled-symbolic.svg + ../data/icons/left-large-symbolic.svg + ../data/icons/arrow-turn-left-down-symbolic.svg + ../data/icons/arrow-circular-top-right-symbolic.svg + ../data/icons/dock-left-symbolic.svg + ../data/icons/server-pick-symbolic.svg + ../data/icons/file-manager-symbolic.svg + ../data/icons/snapshots-alt-symbolic.svg + ../data/icons/arrow-pointing-at-line-down-symbolic.svg + ../data/icons/loupe-large-symbolic.svg + ../data/icons/folder-open-symbolic.svg + ../data/icons/padlock2-symbolic.svg + ../data/icons/pin-symbolic.svg + ../data/icons/pin-small-symbolic.svg + ../data/icons/error-small-symbolic.svg + ../data/icons/copy-symbolic.svg + ../data/icons/double-ended-arrows-vertical-symbolic.svg + ../data/icons/vertical-arrows-long-symbolic.svg + ../data/icons/dot-symbolic.svg + ../data/icons/folder-templates-symbolic.svg + ../data/icons/view-sort-ascending-symbolic.svg + ../data/icons/view-sort-descending-symbolic.svg + ../data/icons/font-x-generic-symbolic.svg + ../data/icons/tag-outline-symbolic.svg + ../data/icons/harddisk-symbolic.svg + ../data/icons/arrow-turn-down-right-symbolic.svg + ../data/icons/minus-large-symbolic.svg + ../data/icons/view-list-bullet-symbolic.svg + ../data/icons/list-remove-all-symbolic.svg + ../data/icons/edit-symbolic.svg diff --git a/src/warehouse.in b/src/warehouse.in index 7590638..1f2ee63 100755 --- a/src/warehouse.in +++ b/src/warehouse.in @@ -36,11 +36,11 @@ locale.textdomain('warehouse') gettext.install('warehouse', localedir) if __name__ == '__main__': - import gi + import gi - from gi.repository import Gio - resource = Gio.Resource.load(os.path.join(pkgdatadir, 'warehouse.gresource')) - resource._register() + from gi.repository import Gio + resource = Gio.Resource.load(os.path.join(pkgdatadir, 'warehouse.gresource')) + resource._register() - from Warehouse import main - sys.exit(main.main(VERSION)) + from Warehouse import main + sys.exit(main.main(VERSION)) diff --git a/warehouse.doap b/warehouse.doap index 241a2ec..6ef279c 100644 --- a/warehouse.doap +++ b/warehouse.doap @@ -1,21 +1,21 @@ + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" + xmlns:foaf="http://xmlns.com/foaf/0.1/" + xmlns:gnome="http://api.gnome.org/doap-extensions#" + xmlns="http://usefulinc.com/ns/doap#"> - Warehouse - Manage all things Flatpak - - - Python - GTK 4 - Libadwaita + Warehouse + Manage all things Flatpak + + + Python + GTK 4 + Libadwaita - - - Heliguy - - - + + + Heliguy + + +