Convert spaces to tabs for indentation

This commit is contained in:
Heliguy
2024-10-27 23:11:35 -04:00
parent 7757069827
commit e416b99c21
40 changed files with 5769 additions and 5769 deletions

View File

@@ -26,8 +26,8 @@
- Permission management - Permission management
- Full fledge package manager / app store - Full fledge package manager / app store
- Viewing app icons and screenshots from remotes - Viewing app icons and screenshots from remotes
- Reading and leaving app reviews - Reading and leaving app reviews
- Auto updates - Auto updates
- Management of any non Flatpak packages - Management of any non Flatpak packages
- Supporting any repackages (apart from nixpkgs should that arise) - Supporting any repackages (apart from nixpkgs should that arise)

View File

@@ -8,31 +8,31 @@
<project_license>GPL-3.0-only</project_license> <project_license>GPL-3.0-only</project_license>
<summary>Manage all things Flatpak</summary> <summary>Manage all things Flatpak</summary>
<description> <description>
<p>Warehouse provides a simple UI to control complex Flatpak options, all without resorting to the command line.</p> <p>Warehouse provides a simple UI to control complex Flatpak options, all without resorting to the command line.</p>
<p>Features:</p> <p>Features:</p>
<ul> <ul>
<li>Manage installed Flatpaks and view properties of any package</li> <li>Manage installed Flatpaks and view properties of any package</li>
<li>Change versions of a Flatpak to rollback any unwanted updates</li> <li>Change versions of a Flatpak to rollback any unwanted updates</li>
<li>Pin runtimes and mask Flatpaks</li> <li>Pin runtimes and mask Flatpaks</li>
<li>Filter packages and sort data, to help find anything easily</li> <li>Filter packages and sort data, to help find anything easily</li>
<li>See current app user data, and cleanup any unused data left behind</li> <li>See current app user data, and cleanup any unused data left behind</li>
<li>Add popular Flatpak remotes with a few clicks or add custom remotes instead</li> <li>Add popular Flatpak remotes with a few clicks or add custom remotes instead</li>
<li>Take snapshots of your apps' user data, saving your data</li> <li>Take snapshots of your apps' user data, saving your data</li>
<li>Install new packages from any remote, or from your system</li> <li>Install new packages from any remote, or from your system</li>
<li>Responsive UI to fit large and small screen sizes</li> <li>Responsive UI to fit large and small screen sizes</li>
</ul> </ul>
</description> </description>
<branding> <branding>
<color type="primary" scheme_preference="light">#AECEF4</color> <color type="primary" scheme_preference="light">#AECEF4</color>
<color type="primary" scheme_preference="dark">#072F5E</color> <color type="primary" scheme_preference="dark">#072F5E</color>
</branding> </branding>
<supports> <supports>
<control>keyboard</control> <control>keyboard</control>
<control>pointing</control> <control>pointing</control>
<control>touch</control> <control>touch</control>
</supports> </supports>
<requires> <requires>
<display_length compare="ge">330</display_length> <display_length compare="ge">330</display_length>
</requires> </requires>
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>
<url type="homepage">https://github.com/flattool/warehouse</url> <url type="homepage">https://github.com/flattool/warehouse</url>
@@ -41,268 +41,268 @@
<url type="translate">https://weblate.fyralabs.com/projects/flattool/warehouse/</url> <url type="translate">https://weblate.fyralabs.com/projects/flattool/warehouse/</url>
<url type="donation">https://ko-fi.com/heliguy</url> <url type="donation">https://ko-fi.com/heliguy</url>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/packages_page_wide.png</image> <image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/packages_page_wide.png</image>
<caption>Manage Installed Packages in Three Pane UI</caption> <caption>Manage Installed Packages in Three Pane UI</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/propteries_page_skinny.png</image> <image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/propteries_page_skinny.png</image>
<caption>Properties Page in Narrow Window</caption> <caption>Properties Page in Narrow Window</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/remotes_page_wide.png</image> <image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/remotes_page_wide.png</image>
<caption>Manage Installed Remotes and Add New Remotes</caption> <caption>Manage Installed Remotes and Add New Remotes</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/data_page_wide.png</image> <image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/data_page_wide.png</image>
<caption>Manage Apps' User Data</caption> <caption>Manage Apps' User Data</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/snapshots_page_wide.png</image> <image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/snapshots_page_wide.png</image>
<caption>Backup Apps' User Data</caption> <caption>Backup Apps' User Data</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_wide.png</image> <image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_wide.png</image>
<caption>Install New Packages from Files or Remotes</caption> <caption>Install New Packages from Files or Remotes</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_skinny.png</image> <image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/install_page_skinny.png</image>
<caption>Install Page in Narrow Window</caption> <caption>Install Page in Narrow Window</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="2.0.0" date="2024-10-28" timestamp="1730014216"> <release version="2.0.0" date="2024-10-28" timestamp="1730014216">
<description translate="no"> <description translate="no">
<p>New Features</p> <p>New Features</p>
<ul> <ul>
<li>All new UI to make using Warehouse's features easier</li> <li>All new UI to make using Warehouse's features easier</li>
<li>UI now better adapts to larger window sizes</li> <li>UI now better adapts to larger window sizes</li>
<li>UI now better adapts to smaller window sizes</li> <li>UI now better adapts to smaller window sizes</li>
<li>Improved UI for installing packages from remotes</li> <li>Improved UI for installing packages from remotes</li>
<li>Snapshots can be given names</li> <li>Snapshots can be given names</li>
<li>Packages can be reinstalled with a few clicks</li> <li>Packages can be reinstalled with a few clicks</li>
<li>User Data can now be sorted in multiple ways</li> <li>User Data can now be sorted in multiple ways</li>
<li>Active User Data can be browsed just as easily as leftover User Data</li> <li>Active User Data can be browsed just as easily as leftover User Data</li>
<li>Custom installation locations are now supported</li> <li>Custom installation locations are now supported</li>
<li>Leftover Snapshots are now shown</li> <li>Leftover Snapshots are now shown</li>
<li>Apps can be reinstalled from leftover Snapshots</li> <li>Apps can be reinstalled from leftover Snapshots</li>
<li>Installation location of disabled remotes is now shown</li> <li>Installation location of disabled remotes is now shown</li>
</ul> </ul>
<p>Changes</p> <p>Changes</p>
<ul> <ul>
<li>Packages list filter options are now easier to understand, and more predictable with how they are applied</li> <li>Packages list filter options are now easier to understand, and more predictable with how they are applied</li>
<li>Improved keyboard shortcuts for quick navigation</li> <li>Improved keyboard shortcuts for quick navigation</li>
<li>The Downgrades (renamed to Change Version) interface now shows the currently installed version</li> <li>The Downgrades (renamed to Change Version) interface now shows the currently installed version</li>
<li>Long running processes now have progress bars and can be canceled</li> <li>Long running processes now have progress bars and can be canceled</li>
<li>Better status icons for End of Life, Masked, and Pinned packages</li> <li>Better status icons for End of Life, Masked, and Pinned packages</li>
<li>Warehouse no longer disables closing its window when long running processes are happening</li> <li>Warehouse no longer disables closing its window when long running processes are happening</li>
<li>Refreshing now shows a loading animation</li> <li>Refreshing now shows a loading animation</li>
</ul> </ul>
<p>Bug Fixes and Performance Improvements</p> <p>Bug Fixes and Performance Improvements</p>
<ul> <ul>
<li>Warehouse is now faster to open</li> <li>Warehouse is now faster to open</li>
<li>Getting system information is now faster</li> <li>Getting system information is now faster</li>
<li>Long running processes no longer freeze the app</li> <li>Long running processes no longer freeze the app</li>
<li>Refreshing is no longer possible when long running processes are happening</li> <li>Refreshing is no longer possible when long running processes are happening</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.6.3" date="2024-7-3" timestamp="1720030786"> <release version="1.6.3" date="2024-7-3" timestamp="1720030786">
<description translate="no"> <description translate="no">
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>Downgrade Window no longer silently fails when downgrading a masked Flatpak, and instead, downgrades it</li> <li>Downgrade Window no longer silently fails when downgrading a masked Flatpak, and instead, downgrades it</li>
<li>When downgrading and masking system Flatpaks, the password prompt only happens once instead of twice</li> <li>When downgrading and masking system Flatpaks, the password prompt only happens once instead of twice</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.6.2" date="2024-4-16" timestamp="1713282319"> <release version="1.6.2" date="2024-4-16" timestamp="1713282319">
<description translate="no"> <description translate="no">
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>Install from the Web no longer has issues on systems with only one remote</li> <li>Install from the Web no longer has issues on systems with only one remote</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.6.1" date="2024-4-7" timestamp="1712531365"> <release version="1.6.1" date="2024-4-7" timestamp="1712531365">
<description translate="no"> <description translate="no">
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>App Properties no longer has issues with information containing colon characters</li> <li>App Properties no longer has issues with information containing colon characters</li>
</ul> </ul>
<p>Previous Releases's New Features and Changes</p> <p>Previous Releases's New Features and Changes</p>
<ul> <ul>
<li>The main list has a status page while refreshing</li> <li>The main list has a status page while refreshing</li>
<li>Add ability to pin and unpin runtimes</li> <li>Add ability to pin and unpin runtimes</li>
<li>App Properties shows license and proper commit information</li> <li>App Properties shows license and proper commit information</li>
<li>App Properties better shows the app's name and description</li> <li>App Properties better shows the app's name and description</li>
<li>Update to GNOME 46 GTK Technologies</li> <li>Update to GNOME 46 GTK Technologies</li>
<li>Updated translations</li> <li>Updated translations</li>
</ul> </ul>
<p>Previous Releases's Bug Fixes</p> <p>Previous Releases's Bug Fixes</p>
<ul> <ul>
<li>When an attempt to run an app fails, correct runtime error information is shown</li> <li>When an attempt to run an app fails, correct runtime error information is shown</li>
<li>Install From The Web no longer behaves incorrectly on remote installations with options</li> <li>Install From The Web no longer behaves incorrectly on remote installations with options</li>
<li>The rare chance that Install From The Web selects a disabled remote has been fixed</li> <li>The rare chance that Install From The Web selects a disabled remote has been fixed</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.6.0" date="2024-4-7" timestamp="1712463635"> <release version="1.6.0" date="2024-4-7" timestamp="1712463635">
<description translate="no"> <description translate="no">
<p>New Features and Changes</p> <p>New Features and Changes</p>
<ul> <ul>
<li>The main list has a status page while refreshing</li> <li>The main list has a status page while refreshing</li>
<li>Add ability to pin and unpin runtimes</li> <li>Add ability to pin and unpin runtimes</li>
<li>App Properties shows license and proper commit information</li> <li>App Properties shows license and proper commit information</li>
<li>App Properties better shows the app's name and description</li> <li>App Properties better shows the app's name and description</li>
<li>Update to GNOME 46 GTK Technologies</li> <li>Update to GNOME 46 GTK Technologies</li>
<li>Updated translations</li> <li>Updated translations</li>
</ul> </ul>
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>When an attempt to run an app fails, correct runtime error information is shown</li> <li>When an attempt to run an app fails, correct runtime error information is shown</li>
<li>Install From The Web no longer behaves incorrectly on remote installations with options</li> <li>Install From The Web no longer behaves incorrectly on remote installations with options</li>
<li>The rare chance that Install From The Web selects a disabled remote has been fixed</li> <li>The rare chance that Install From The Web selects a disabled remote has been fixed</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.5.1" date="2024-3-8" timestamp="1709921475"> <release version="1.5.1" date="2024-3-8" timestamp="1709921475">
<description translate="no"> <description translate="no">
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>Main list is no longer scrolled to the bottom on launch</li> <li>Main list is no longer scrolled to the bottom on launch</li>
<li>Leftover Data window no longer tries to use a different window for toast messages</li> <li>Leftover Data window no longer tries to use a different window for toast messages</li>
<li>Fix issue causing Downgrade window to not be able to downgrade user installation Flatpaks</li> <li>Fix issue causing Downgrade window to not be able to downgrade user installation Flatpaks</li>
<li>Fix the accidental removal of translations</li> <li>Fix the accidental removal of translations</li>
<li>Fix issue causing Reset Filters button to be clickable upon Filter Window opening even when the filters are default</li> <li>Fix issue causing Reset Filters button to be clickable upon Filter Window opening even when the filters are default</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.5.0" date="2024-3-5" timestamp="1709694300"> <release version="1.5.0" date="2024-3-5" timestamp="1709694300">
<description translate="no"> <description translate="no">
<p>New Features and Changes</p> <p>New Features and Changes</p>
<ul> <ul>
<li>Flatpaks can now be installed from the web, from any added remote</li> <li>Flatpaks can now be installed from the web, from any added remote</li>
<li>Filters are now saved and restored between sessions</li> <li>Filters are now saved and restored between sessions</li>
<li>Filters now apply live</li> <li>Filters now apply live</li>
<li>Move Refresh Button into Main Menu</li> <li>Move Refresh Button into Main Menu</li>
<li>Period, 0 to 9, and underscores are now allowed in new Custom Remote names</li> <li>Period, 0 to 9, and underscores are now allowed in new Custom Remote names</li>
<li>Updated translations</li> <li>Updated translations</li>
</ul> </ul>
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>Hide Show Disabled Remotes button when there aren't any</li> <li>Hide Show Disabled Remotes button when there aren't any</li>
<li>Fix Batch Snapshots accidentally triggering Select All</li> <li>Fix Batch Snapshots accidentally triggering Select All</li>
<li>Misc UI element tooltips and alignments</li> <li>Misc UI element tooltips and alignments</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.4.0" date="2023-12-17" timestamp="1702875630"> <release version="1.4.0" date="2023-12-17" timestamp="1702875630">
<description translate="no"> <description translate="no">
<p>New Features and Changes</p> <p>New Features and Changes</p>
<ul> <ul>
<li>View disabled remotes, enable and disable remotes, and set a filter for a remote in the Manage Remotes window</li> <li>View disabled remotes, enable and disable remotes, and set a filter for a remote in the Manage Remotes window</li>
<li>Added a new Snapshot feature. Snapshots can be created and applied at any time for quick saving app user data</li> <li>Added a new Snapshot feature. Snapshots can be created and applied at any time for quick saving app user data</li>
<li>Added a batch action to create Snapshots</li> <li>Added a batch action to create Snapshots</li>
<li>Revamped the Properties window, and it can also open app details in the software store</li> <li>Revamped the Properties window, and it can also open app details in the software store</li>
<li>Merged the Popular Remotes window into the Manage Remotes window</li> <li>Merged the Popular Remotes window into the Manage Remotes window</li>
<li>Removed labels in the main list, placing them in popup text buttons instead, to save room</li> <li>Removed labels in the main list, placing them in popup text buttons instead, to save room</li>
<li>Added progress bars to show progress of batch actions</li> <li>Added progress bars to show progress of batch actions</li>
<li>Empty search pages now display a message</li> <li>Empty search pages now display a message</li>
<li>Added a troubleshooting information page</li> <li>Added a troubleshooting information page</li>
<li>Added a donation link</li> <li>Added a donation link</li>
<li>Added translations (NL, FR, RU, SV, UK)</li> <li>Added translations (NL, FR, RU, SV, UK)</li>
</ul> </ul>
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>Launching Warehouse no longer hangs when grabbing the list of Flatpaks</li> <li>Launching Warehouse no longer hangs when grabbing the list of Flatpaks</li>
<li>Unexpected errors are caught more often and handled better</li> <li>Unexpected errors are caught more often and handled better</li>
<li>Fixed a few typos</li> <li>Fixed a few typos</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.3.0" date="2023-10-23" timestamp="1698112217"> <release version="1.3.0" date="2023-10-23" timestamp="1698112217">
<description translate="no"> <description translate="no">
<p>New Features and Changes</p> <p>New Features and Changes</p>
<ul> <ul>
<li>Names, IDs, Refs, and Launch Commands can now be copied from a dropdown</li> <li>Names, IDs, Refs, and Launch Commands can now be copied from a dropdown</li>
<li>Updates can now be disabled and enabled for Flatpaks</li> <li>Updates can now be disabled and enabled for Flatpaks</li>
<li>Warehouse can now downgrade Flatpaks</li> <li>Warehouse can now downgrade Flatpaks</li>
<li>Apps can be now be ran from Warehouse</li> <li>Apps can be now be ran from Warehouse</li>
<li>Control + Keypad Enter now also toggles select mode</li> <li>Control + Keypad Enter now also toggles select mode</li>
<li>Added translations (HG, ES, TH)</li> <li>Added translations (HG, ES, TH)</li>
</ul> </ul>
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>The runtime filter button in the properties window now only shows when that runtime is a dependent runtime</li> <li>The runtime filter button in the properties window now only shows when that runtime is a dependent runtime</li>
<li>Removed the Select All keyboard shortcut as it interfered with the search bar</li> <li>Removed the Select All keyboard shortcut as it interfered with the search bar</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.2.1" date="2023-10-15" timestamp="1697405182"> <release version="1.2.1" date="2023-10-15" timestamp="1697405182">
<description translate="no"> <description translate="no">
<ul> <ul>
<li>The main list of Flatpaks is now sorted by name instead of ID</li> <li>The main list of Flatpaks is now sorted by name instead of ID</li>
<li>Warehouse can now be found in your app menu by searching for "flatpak"</li> <li>Warehouse can now be found in your app menu by searching for "flatpak"</li>
<li>Corrected typo in the previous release's change log</li> <li>Corrected typo in the previous release's change log</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.2.0" date="2023-10-11" timestamp="1697076304"> <release version="1.2.0" date="2023-10-11" timestamp="1697076304">
<description translate="no"> <description translate="no">
<p>New Features and Changes</p> <p>New Features and Changes</p>
<ul> <ul>
<li>Flatpaks files can be installed with a drag and drop or from a file selection</li> <li>Flatpaks files can be installed with a drag and drop or from a file selection</li>
<li>Flatpak remotes can be added with a drag and drop or from a file selection</li> <li>Flatpak remotes can be added with a drag and drop or from a file selection</li>
<li>Apps can now be filtered by dependent runtimes</li> <li>Apps can now be filtered by dependent runtimes</li>
<li>Properties of a runtime now shows a button to show only apps that rely on the runtime</li> <li>Properties of a runtime now shows a button to show only apps that rely on the runtime</li>
<li>A loading indicator is now shown when adding a remote</li> <li>A loading indicator is now shown when adding a remote</li>
<li>Added Webkit Testing to the list of popular remotes</li> <li>Added Webkit Testing to the list of popular remotes</li>
<li>Added a search bar to the Leftover Data window</li> <li>Added a search bar to the Leftover Data window</li>
<li>Added a button to open the entire user data folder the Leftover Data window</li> <li>Added a button to open the entire user data folder the Leftover Data window</li>
<li>Added a button on each row in the Leftover Data list to open them directly</li> <li>Added a button on each row in the Leftover Data list to open them directly</li>
<li>Changed margins of lists to improve legibility</li> <li>Changed margins of lists to improve legibility</li>
<li>F10 now opens the main menu</li> <li>F10 now opens the main menu</li>
</ul> </ul>
<p>Bug Fixes and Stability Improvements</p> <p>Bug Fixes and Stability Improvements</p>
<ul> <ul>
<li>Fixed a crash that would sometimes occur when opening the Leftover Data window</li> <li>Fixed a crash that would sometimes occur when opening the Leftover Data window</li>
<li>Popular remotes are now named correcting and have proper descriptions</li> <li>Popular remotes are now named correcting and have proper descriptions</li>
<li>The filter button now disables when the Filter window is closed by the keyboard</li> <li>The filter button now disables when the Filter window is closed by the keyboard</li>
<li>The default filter is no longer allowed to be set as a new filter</li> <li>The default filter is no longer allowed to be set as a new filter</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.1.1" date="2023-10-6" timestamp="1696572524"> <release version="1.1.1" date="2023-10-6" timestamp="1696572524">
<description translate="no"> <description translate="no">
<p>Emergency Bug Fix</p> <p>Emergency Bug Fix</p>
<ul> <ul>
<li>Fix error causing a crash on Linux Mint</li> <li>Fix error causing a crash on Linux Mint</li>
<li>Correct typo in the app summary</li> <li>Correct typo in the app summary</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.1.0" date="2023-10-4" timestamp="1696461734"> <release version="1.1.0" date="2023-10-4" timestamp="1696461734">
<description translate="no"> <description translate="no">
<p>New Features and Changes</p> <p>New Features and Changes</p>
<ul> <ul>
<li>Choose from a list of popular remotes when adding a new remote</li> <li>Choose from a list of popular remotes when adding a new remote</li>
<li>App properties now shows the runtime that the app relies on</li> <li>App properties now shows the runtime that the app relies on</li>
<li>Apps and runtimes that are End of Life are now noted as such</li> <li>Apps and runtimes that are End of Life are now noted as such</li>
<li>Window size and state is remembered between sessions</li> <li>Window size and state is remembered between sessions</li>
</ul> </ul>
<p>Bug Fixes and Performance Improvements</p> <p>Bug Fixes and Performance Improvements</p>
<ul> <ul>
<li>The UI no longer freezes when uninstalling apps</li> <li>The UI no longer freezes when uninstalling apps</li>
<li>The UI no longer freezes when getting file sizes of large files</li> <li>The UI no longer freezes when getting file sizes of large files</li>
<li>Toggling batch mode no longer causes a freeze</li> <li>Toggling batch mode no longer causes a freeze</li>
<li>Selecting all apps no longer causes a freeze</li> <li>Selecting all apps no longer causes a freeze</li>
<li>Applying and removing a filter no longer causes a freeze</li> <li>Applying and removing a filter no longer causes a freeze</li>
<li>Fixed issue where the no remotes status page would not be removed when a new remote was added</li> <li>Fixed issue where the no remotes status page would not be removed when a new remote was added</li>
</ul> </ul>
</description> </description>
</release> </release>
<release version="1.0.0" date="2023-9-25" timestamp="1695695940"> <release version="1.0.0" date="2023-9-25" timestamp="1695695940">
<description translate="no"> <description translate="no">
<p>First release of Warehouse</p> <p>First release of Warehouse</p>
</description> </description>

View File

@@ -1,55 +1,55 @@
{ {
"id": "io.github.flattool.Warehouse", "id": "io.github.flattool.Warehouse",
"runtime": "org.gnome.Platform", "runtime": "org.gnome.Platform",
"runtime-version": "47", "runtime-version": "47",
"sdk": "org.gnome.Sdk", "sdk": "org.gnome.Sdk",
"command": "warehouse", "command": "warehouse",
"finish-args": [ "finish-args": [
"--share=ipc", "--share=ipc",
"--socket=fallback-x11", "--socket=fallback-x11",
"--device=dri", "--device=dri",
"--socket=wayland", "--socket=wayland",
"--talk-name=org.freedesktop.Flatpak", "--talk-name=org.freedesktop.Flatpak",
"--filesystem=/var/lib/flatpak/:ro", "--filesystem=/var/lib/flatpak/:ro",
"--filesystem=~/.local/share/flatpak/:ro", "--filesystem=~/.local/share/flatpak/:ro",
"--filesystem=~/.var/app/", "--filesystem=~/.var/app/",
"--filesystem=host-etc" "--filesystem=host-etc"
], ],
"cleanup": [ "cleanup": [
"/include", "/include",
"/lib/pkgconfig", "/lib/pkgconfig",
"/man", "/man",
"/share/doc", "/share/doc",
"/share/gtk-doc", "/share/gtk-doc",
"/share/man", "/share/man",
"/share/pkgconfig", "/share/pkgconfig",
"*.la", "*.la",
"*.a" "*.a"
], ],
"modules": [ "modules": [
{ {
"name": "blueprint-compiler", "name": "blueprint-compiler",
"buildsystem": "meson", "buildsystem": "meson",
"sources": [ "sources": [
{ {
"type": "git", "type": "git",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler",
"tag": "v0.14.0" "tag": "v0.14.0"
} }
], ],
"cleanup": ["*"] "cleanup": ["*"]
}, },
{ {
"name": "warehouse", "name": "warehouse",
"builddir": true, "builddir": true,
"buildsystem": "meson", "buildsystem": "meson",
"config-opts": ["-Dprofile=development"], "config-opts": ["-Dprofile=development"],
"sources": [ "sources": [
{ {
"type": "dir", "type": "dir",
"path": "." "path": "."
} }
] ]
} }
] ]
} }

View File

@@ -7,118 +7,118 @@ import subprocess
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/change_version_page/change_version_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/change_version_page/change_version_page.ui")
class ChangeVersionPage(Adw.NavigationPage): class ChangeVersionPage(Adw.NavigationPage):
__gtype_name__ = 'ChangeVersionPage' __gtype_name__ = 'ChangeVersionPage'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
toast_overlay = gtc() toast_overlay = gtc()
scrolled_window = gtc() scrolled_window = gtc()
versions_clamp = gtc() versions_clamp = gtc()
root_group_check_button = gtc() root_group_check_button = gtc()
mask_group = gtc() mask_group = gtc()
mask_row = gtc() mask_row = gtc()
versions_group = gtc() versions_group = gtc()
action_bar = gtc() action_bar = gtc()
apply_button = gtc() apply_button = gtc()
selected_commit = None selected_commit = None
failure = None failure = None
def get_commits(self, *args): def get_commits(self, *args):
cmd = ['flatpak-spawn', '--host', 'sh', '-c'] cmd = ['flatpak-spawn', '--host', 'sh', '-c']
script = f"LC_ALL=C flatpak remote-info --log {self.package.info['origin']} {self.package.info['ref']} " script = f"LC_ALL=C flatpak remote-info --log {self.package.info['origin']} {self.package.info['ref']} "
installation = self.package.info["installation"] installation = self.package.info["installation"]
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
script += f"--{installation}" script += f"--{installation}"
else: else:
script += f"--installation={installation}" script += f"--installation={installation}"
cmd.append(script) cmd.append(script)
commits = [] commits = []
changes = [] changes = []
dates = [] dates = []
try: try:
output = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout output = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout
lines = output.strip().split('\n') lines = output.strip().split('\n')
for line in lines: for line in lines:
line = line.strip().split(": ", 1) line = line.strip().split(": ", 1)
if len(line) < 2: if len(line) < 2:
continue continue
elif line[0].startswith("Commit"): elif line[0].startswith("Commit"):
commits.append(line[1]) commits.append(line[1])
elif line[0].startswith("Subject"): elif line[0].startswith("Subject"):
changes.append(line[1]) changes.append(line[1])
elif line[0].startswith("Date"): elif line[0].startswith("Date"):
dates.append(line[1]) dates.append(line[1])
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.failure = cpe.stderr self.failure = cpe.stderr
return return
except Exception as e: except Exception as e:
self.failure = str(e) self.failure = str(e)
return return
if not (len(commits) == len(changes) == len(dates)): if not (len(commits) == len(changes) == len(dates)):
self.failure = "Commits, Changes, and Dates are not of equivalent length" self.failure = "Commits, Changes, and Dates are not of equivalent length"
return return
def idle(*args): def idle(*args):
for index, commit in enumerate(commits): for index, commit in enumerate(commits):
row = Adw.ActionRow(title=GLib.markup_escape_text(changes[index]), subtitle=f"{GLib.markup_escape_text(commit)}\n{GLib.markup_escape_text(dates[index])}") row = Adw.ActionRow(title=GLib.markup_escape_text(changes[index]), subtitle=f"{GLib.markup_escape_text(commit)}\n{GLib.markup_escape_text(dates[index])}")
if commit == self.package.cli_info.get("commit", None): if commit == self.package.cli_info.get("commit", None):
row.set_sensitive(False) row.set_sensitive(False)
row.add_prefix(Gtk.Image(icon_name="check-plain-symbolic", margin_start=5, margin_end=5)) row.add_prefix(Gtk.Image(icon_name="check-plain-symbolic", margin_start=5, margin_end=5))
row.set_tooltip_text(_("Currently Installed Version")) row.set_tooltip_text(_("Currently Installed Version"))
else: else:
check = Gtk.CheckButton() check = Gtk.CheckButton()
check.connect("activate", lambda *_, comm=commit: self.set_commit(comm)) check.connect("activate", lambda *_, comm=commit: self.set_commit(comm))
check.set_group(self.root_group_check_button) check.set_group(self.root_group_check_button)
row.set_activatable_widget(check) row.set_activatable_widget(check)
row.add_prefix(check) row.add_prefix(check)
self.versions_group.add(row) self.versions_group.add(row)
GLib.idle_add(idle) GLib.idle_add(idle)
def set_commit(self, commit): def set_commit(self, commit):
self.selected_commit = commit self.selected_commit = commit
def get_commits_callback(self, *args): def get_commits_callback(self, *args):
if not self.failure is None: if not self.failure is None:
self.toast_overlay.add_toast(ErrorToast(_("Could not get versions"), self.failure).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not get versions"), self.failure).toast)
else: else:
self.scrolled_window.set_child(self.versions_clamp) self.scrolled_window.set_child(self.versions_clamp)
def callback(self, did_error): def callback(self, did_error):
HostInfo.main_window.refresh_handler() HostInfo.main_window.refresh_handler()
if not did_error: if not did_error:
HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Changed {}'s Version").format(self.package.info['name']))) HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Changed {}'s Version").format(self.package.info['name'])))
def error_callback(self, user_facing_label, error_message): def error_callback(self, user_facing_label, error_message):
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast) HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast)
def on_apply(self, *args): def on_apply(self, *args):
if ChangeVersionWorker.change_version( if ChangeVersionWorker.change_version(
self.mask_row.get_active(), self.mask_row.get_active(),
self.package, self.selected_commit, self.package, self.selected_commit,
self.packages_page.changing_version, self.packages_page.changing_version,
self.callback, self.callback,
self.error_callback, self.error_callback,
): ):
self.packages_page.set_status(self.packages_page.changing_version) self.packages_page.set_status(self.packages_page.changing_version)
def __init__(self, packages_page, package, **kwargs): def __init__(self, packages_page, package, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.packages_page = packages_page self.packages_page = packages_page
self.package = package self.package = package
# Apply # Apply
pkg_name = package.info["name"] pkg_name = package.info["name"]
self.set_title(_("{} Versions").format(pkg_name)) self.set_title(_("{} Versions").format(pkg_name))
self.mask_row.set_subtitle(_("Ensure that {} will never be updated to a newer version").format(pkg_name)) self.mask_row.set_subtitle(_("Ensure that {} will never be updated to a newer version").format(pkg_name))
self.scrolled_window.set_child(LoadingStatus(_("Fetching Releases"), _("This could take a while"))) self.scrolled_window.set_child(LoadingStatus(_("Fetching Releases"), _("This could take a while")))
Gio.Task.new(None, None, self.get_commits_callback).run_in_thread(self.get_commits) Gio.Task.new(None, None, self.get_commits_callback).run_in_thread(self.get_commits)
# Connections # Connections
self.root_group_check_button.connect("toggled", lambda *_: self.action_bar.set_revealed(True)) self.root_group_check_button.connect("toggled", lambda *_: self.action_bar.set_revealed(True))
self.apply_button.connect("clicked", self.on_apply) self.apply_button.connect("clicked", self.on_apply)

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only # SPDX-License-Identifier: GPL-3.0-only
class Config: class Config:
DEVEL = '@DEVEL@' == 'Development' DEVEL = '@DEVEL@' == 'Development'
PROFILE = '@DEVEL@' PROFILE = '@DEVEL@'
APP_ID = '@APPID@' APP_ID = '@APPID@'
VERSION = '@VERSION@' VERSION = '@VERSION@'

View File

@@ -2,43 +2,43 @@ from gi.repository import Adw, Gtk, GLib
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/gtk/app_row.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/gtk/app_row.ui")
class AppRow(Adw.ActionRow): class AppRow(Adw.ActionRow):
__gtype_name__ = 'AppRow' __gtype_name__ = 'AppRow'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
image = gtc() image = gtc()
eol_package_package_status_icon = gtc() eol_package_package_status_icon = gtc()
eol_runtime_status_icon = gtc() eol_runtime_status_icon = gtc()
pinned_status_icon = gtc() pinned_status_icon = gtc()
masked_status_icon = gtc() masked_status_icon = gtc()
check_button = gtc() check_button = gtc()
def idle_stuff(self): def idle_stuff(self):
if self.package.icon_path: if self.package.icon_path:
self.image.add_css_class("icon-dropshadow") self.image.add_css_class("icon-dropshadow")
self.image.set_from_file(self.package.icon_path) self.image.set_from_file(self.package.icon_path)
def gesture_handler(self, *args): def gesture_handler(self, *args):
if self.on_long_press: if self.on_long_press:
self.on_long_press(self) self.on_long_press(self)
def __init__(self, package, on_long_press=None, **kwargs): def __init__(self, package, on_long_press=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.package = package self.package = package
self.on_long_press = on_long_press self.on_long_press = on_long_press
self.rclick_gesture = Gtk.GestureClick(button=3) self.rclick_gesture = Gtk.GestureClick(button=3)
self.long_press_gesture = Gtk.GestureLongPress() self.long_press_gesture = Gtk.GestureLongPress()
# Apply # Apply
GLib.idle_add(lambda *_: self.set_title(package.info["name"])) 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.set_subtitle(package.info["id"]))
GLib.idle_add(lambda *_: self.idle_stuff()) GLib.idle_add(lambda *_: self.idle_stuff())
self.add_controller(self.rclick_gesture) self.add_controller(self.rclick_gesture)
self.add_controller(self.long_press_gesture) self.add_controller(self.long_press_gesture)
if package.info['id'] == "io.github.flattool.Warehouse": if package.info['id'] == "io.github.flattool.Warehouse":
self.check_button.set_active = lambda *_: None self.check_button.set_active = lambda *_: None
self.check_button.set_sensitive(False) self.check_button.set_sensitive(False)
# Connections # Connections
self.rclick_gesture.connect("released", self.gesture_handler) self.rclick_gesture.connect("released", self.gesture_handler)
self.long_press_gesture.connect("pressed", self.gesture_handler) self.long_press_gesture.connect("pressed", self.gesture_handler)

View File

@@ -1,29 +1,29 @@
from gi.repository import Adw, Gtk, Gdk, GLib from gi.repository import Adw, Gtk, Gdk, GLib
class ErrorToast: class ErrorToast:
main_window = None main_window = None
def __init__(self, display_msg, error_msg): def __init__(self, display_msg, error_msg):
def on_response(dialog, response_id): def on_response(dialog, response_id):
if response_id == "copy": if response_id == "copy":
self.clipboard.set(error_msg) self.clipboard.set(error_msg)
# Extra Object Creation # Extra Object Creation
self.toast = Adw.Toast(title=display_msg, button_label=_("Details")) self.toast = Adw.Toast(title=display_msg, button_label=_("Details"))
popup = Adw.AlertDialog.new(display_msg) popup = Adw.AlertDialog.new(display_msg)
self.clipboard = Gdk.Display.get_default().get_clipboard() self.clipboard = Gdk.Display.get_default().get_clipboard()
# Apply # Apply
print(display_msg) print(display_msg)
print(error_msg) print(error_msg)
popup.add_response("copy", _("Copy")) popup.add_response("copy", _("Copy"))
popup.add_response("ok", _("OK")) popup.add_response("ok", _("OK"))
lb = Gtk.Label(selectable=True, wrap=True)#, natural_wrap_mode=Gtk.NaturalWrapMode.WORD) lb = Gtk.Label(selectable=True, wrap=True)#, natural_wrap_mode=Gtk.NaturalWrapMode.WORD)
lb.set_markup(f"<tt>{GLib.markup_escape_text(error_msg)}</tt>") lb.set_markup(f"<tt>{GLib.markup_escape_text(error_msg)}</tt>")
# lb.set_label(error_msg) # lb.set_label(error_msg)
# lb.set_selectable(True) # lb.set_selectable(True)
popup.set_extra_child(lb) popup.set_extra_child(lb)
# Connections # Connections
self.toast.connect("button-clicked", lambda *_: popup.present(self.main_window)) self.toast.connect("button-clicked", lambda *_: popup.present(self.main_window))
popup.connect("response", on_response) popup.connect("response", on_response)

View File

@@ -9,376 +9,376 @@ direction = Gtk.Image().get_direction()
class Flatpak: class Flatpak:
def open_app(self, callback=None): def open_app(self, callback=None):
self.failed_app_run = None self.failed_app_run = None
def thread(*args): def thread(*args):
if self.is_runtime: if self.is_runtime:
self.failed_app_run = "error: cannot open a runtime" self.failed_app_run = "error: cannot open a runtime"
try: try:
subprocess.run(['flatpak-spawn', '--host', 'flatpak', 'run', f"{self.info['ref']}"], capture_output=True, text=True, check=True) subprocess.run(['flatpak-spawn', '--host', 'flatpak', 'run', f"{self.info['ref']}"], capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.failed_app_run = cpe self.failed_app_run = cpe
except Exception as e: except Exception as e:
self.failed_app_run = 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): def open_data(self):
if not os.path.exists(self.data_path): if not os.path.exists(self.data_path):
return f"Path '{self.data_path}' does not exist" return f"Path '{self.data_path}' does not exist"
try: try:
Gio.AppInfo.launch_default_for_uri(f"file://{self.data_path}", None) Gio.AppInfo.launch_default_for_uri(f"file://{self.data_path}", None)
except GLib.GError as e: except GLib.GError as e:
return e return e
def get_data_size(self, callback=None): def get_data_size(self, callback=None):
size = [None] size = [None]
def thread(*args): def thread(*args):
sed = "sed 's/K/ KB/; s/M/ MB/; s/G/ GB/; s/T/ TB/; s/P/ PB/;'" 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] 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): def on_done(*arg):
if callback: if callback:
callback(f"~ {size[0]}") callback(f"~ {size[0]}")
Gio.Task.new(None, None, on_done).run_in_thread(thread) Gio.Task.new(None, None, on_done).run_in_thread(thread)
def trash_data(self, callback=None): def trash_data(self, callback=None):
try: try:
subprocess.run(['gio', 'trash', self.data_path], capture_output=True, text=True, check=True) subprocess.run(['gio', 'trash', self.data_path], capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
raise cpe raise cpe
except Exception as e: except Exception as e:
raise e raise e
def set_mask(self, should_mask, callback=None): def set_mask(self, should_mask, callback=None):
self.failed_mask = None self.failed_mask = None
def thread(*args): def thread(*args):
cmd = ['flatpak-spawn', '--host', 'flatpak', 'mask', self.info["id"]] cmd = ['flatpak-spawn', '--host', 'flatpak', 'mask', self.info["id"]]
installation = self.info["installation"] installation = self.info["installation"]
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
if not should_mask: if not should_mask:
cmd.append("--remove") cmd.append("--remove")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
self.is_masked = should_mask self.is_masked = should_mask
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.failed_mask = cpe self.failed_mask = cpe
except Exception as e: except Exception as e:
self.failed_mask = 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): def set_pin(self, should_pin, callback=None):
self.failed_pin = None self.failed_pin = None
if not self.is_runtime: if not self.is_runtime:
self.failed_pin = "Cannot pin an application" self.failed_pin = "Cannot pin an application"
def thread(*args): def thread(*args):
cmd = ['flatpak-spawn', '--host', 'flatpak', 'pin', f"runtime/{self.info['ref']}"] cmd = ['flatpak-spawn', '--host', 'flatpak', 'pin', f"runtime/{self.info['ref']}"]
installation = self.info["installation"] installation = self.info["installation"]
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
if not should_pin: if not should_pin:
cmd.append("--remove") cmd.append("--remove")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.failed_pin = cpe self.failed_pin = cpe
except Exception as e: except Exception as e:
self.failed_mask = 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): def uninstall(self, callee_callback=None):
self.failed_uninstall = None self.failed_uninstall = None
def callback(*args): def callback(*args):
HostInfo.main_window.remove_refresh_lockout("uninstalling packages") HostInfo.main_window.remove_refresh_lockout("uninstalling packages")
if not callee_callback is None: if not callee_callback is None:
callee_callback() callee_callback()
def thread(*args): def thread(*args):
HostInfo.main_window.add_refresh_lockout("uninstalling packages") HostInfo.main_window.add_refresh_lockout("uninstalling packages")
cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y', self.info["ref"]] cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y', self.info["ref"]]
installation = self.info["installation"] installation = self.info["installation"]
if installation == "system" or installation == "user": if installation == "system" or installation == "user":
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
try: try:
subprocess.run(cmd, check=True, text=True, capture_output=True) subprocess.run(cmd, check=True, text=True, capture_output=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.failed_uninstall = cpe self.failed_uninstall = cpe
except Exception as e: except Exception as e:
self.failed_uninstall = 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): def get_cli_info(self):
cli_info = {} cli_info = {}
cmd = "LC_ALL=C flatpak info " cmd = "LC_ALL=C flatpak info "
installation = self.info["installation"] installation = self.info["installation"]
if installation == "user": if installation == "user":
cmd += "--user " cmd += "--user "
elif installation == "system": elif installation == "system":
cmd += "--system " cmd += "--system "
else: else:
cmd += f"--installation={installation} " cmd += f"--installation={installation} "
cmd += self.info["ref"] cmd += self.info["ref"]
try: try:
output = subprocess.run( output = subprocess.run(
['flatpak-spawn', '--host', 'sh', '-c', cmd], ['flatpak-spawn', '--host', 'sh', '-c', cmd],
text=True, capture_output=True text=True, capture_output=True
).stdout ).stdout
except Exception as e: except Exception as e:
raise e raise e
lines = output.strip().split("\n") lines = output.strip().split("\n")
cli_info["description"] = "" cli_info["description"] = ""
first = lines.pop(0) first = lines.pop(0)
if " - " in first: if " - " in first:
cli_info["description"] = first.split(" - ")[1] cli_info["description"] = first.split(" - ")[1]
# Handle descriptions that contain newlines # Handle descriptions that contain newlines
while (line := lines.pop(0)) and not ":" in line: while (line := lines.pop(0)) and not ":" in line:
if len(line) > 0: if len(line) > 0:
cli_info["description"] += f" {line}" cli_info["description"] += f" {line}"
for i, word in enumerate(lines): for i, word in enumerate(lines):
if not ":" in word: if not ":" in word:
continue continue
word = word.strip().split(": ", 1) word = word.strip().split(": ", 1)
if len(word) < 2: if len(word) < 2:
continue continue
word[0] = word[0].lower() word[0] = word[0].lower()
if "installed" in word[0]: if "installed" in word[0]:
word[1] = word[1].replace("?", " ") word[1] = word[1].replace("?", " ")
cli_info[word[0]] = word[1] cli_info[word[0]] = word[1]
self.cli_info = cli_info self.cli_info = cli_info
return cli_info return cli_info
def __init__(self, columns): def __init__(self, columns):
self.info = { self.info = {
"name": columns[0], "name": columns[0],
"id": columns[1], "id": columns[1],
"version": columns[2], "version": columns[2],
"branch": columns[3], "branch": columns[3],
"arch": columns[4], "arch": columns[4],
"origin": columns[5], "origin": columns[5],
"installation": columns[6], "installation": columns[6],
"ref": columns[7], "ref": columns[7],
"installed_size": columns[8], "installed_size": columns[8],
"options": columns[9], "options": columns[9],
} }
self.is_runtime = "runtime" in self.info["options"] self.is_runtime = "runtime" in self.info["options"]
self.data_path = f"{home}/.var/app/{self.info["id"]}" self.data_path = f"{home}/.var/app/{self.info["id"]}"
self.data_size = -1 self.data_size = -1
self.cli_info = None self.cli_info = None
installation = self.info["installation"] installation = self.info["installation"]
if len(i := installation.split(' ')) > 1: if len(i := installation.split(' ')) > 1:
self.info["installation"] = i[1].replace("(", "").replace(")", "") self.info["installation"] = i[1].replace("(", "").replace(")", "")
else: else:
self.info["installation"] = installation self.info["installation"] = installation
self.is_eol = "eol=" in self.info["options"] self.is_eol = "eol=" in self.info["options"]
self.dependent_runtime = None self.dependent_runtime = None
self.failed_app_run = None self.failed_app_run = None
self.failed_mask = None self.failed_mask = None
self.failed_uninstall = None self.failed_uninstall = None
self.app_row = None self.app_row = None
try: try:
self.is_masked = self.info["id"] in HostInfo.masks[self.info["installation"]] self.is_masked = self.info["id"] in HostInfo.masks[self.info["installation"]]
except KeyError: except KeyError:
self.is_masked = False self.is_masked = False
try: try:
self.is_pinned = f"runtime/{self.info['ref']}" in HostInfo.pins[self.info["installation"]] self.is_pinned = f"runtime/{self.info['ref']}" in HostInfo.pins[self.info["installation"]]
except KeyError: except KeyError:
self.is_pinned = False self.is_pinned = False
try: try:
self.icon_path = ( self.icon_path = (
icon_theme.lookup_icon( icon_theme.lookup_icon(
self.info["id"], None, 512, 1, direction, 0 self.info["id"], None, 512, 1, direction, 0
) )
.get_file() .get_file()
.get_path() .get_path()
) )
except GLib.GError as e: except GLib.GError as e:
print(f"Minor error in looking up icon for {self.info['id']}", e) print(f"Minor error in looking up icon for {self.info['id']}", e)
self.icon_path = None self.icon_path = None
class Remote: class Remote:
def __init__(self, name, title, disabled): def __init__(self, name, title, disabled):
self.name = name self.name = name
self.title = title self.title = title
self.disabled = disabled self.disabled = disabled
if title == "" or title == "-": if title == "" or title == "-":
self.title = name self.title = name
class HostInfo: class HostInfo:
home = home home = home
clipboard = Gdk.Display.get_default().get_clipboard() clipboard = Gdk.Display.get_default().get_clipboard()
main_window = None main_window = None
snapshots_path = f"{home}/.var/app/io.github.flattool.Warehouse/data/Snapshots/" snapshots_path = f"{home}/.var/app/io.github.flattool.Warehouse/data/Snapshots/"
# Get all possible installation icon theme dirs # Get all possible installation icon theme dirs
output = subprocess.run( output = subprocess.run(
['flatpak-spawn', '--host', ['flatpak-spawn', '--host',
'flatpak', '--installations'], 'flatpak', '--installations'],
text=True, text=True,
capture_output=True, capture_output=True,
).stdout ).stdout
lines = output.strip().split("\n") lines = output.strip().split("\n")
for i in lines: for i in lines:
icon_theme.add_search_path(f"{i}/exports/share/icons") icon_theme.add_search_path(f"{i}/exports/share/icons")
flatpaks = [] flatpaks = []
id_to_flatpak = {} id_to_flatpak = {}
ref_to_flatpak = {} ref_to_flatpak = {}
remotes = {} remotes = {}
installations = [] installations = []
masks = {} masks = {}
pins = {} pins = {}
dependent_runtime_refs = [] dependent_runtime_refs = []
@classmethod @classmethod
def get_flatpaks(this, callback=None): def get_flatpaks(this, callback=None):
# Callback is a function to run after the host flatpaks are found # Callback is a function to run after the host flatpaks are found
this.flatpaks.clear() this.flatpaks.clear()
this.id_to_flatpak.clear() this.id_to_flatpak.clear()
this.ref_to_flatpak.clear() this.ref_to_flatpak.clear()
this.remotes.clear() this.remotes.clear()
this.installations.clear() this.installations.clear()
this.masks.clear() this.masks.clear()
this.pins.clear() this.pins.clear()
this.dependent_runtime_refs.clear() this.dependent_runtime_refs.clear()
def thread(task, *args): def thread(task, *args):
# Remotes # Remotes
def remote_info(installation): def remote_info(installation):
cmd = ['flatpak-spawn', '--host', cmd = ['flatpak-spawn', '--host',
'flatpak', 'remotes', '--columns=name,title,options', '--show-disabled'] 'flatpak', 'remotes', '--columns=name,title,options', '--show-disabled']
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
output = subprocess.run( output = subprocess.run(
cmd, text=True, cmd, text=True,
capture_output=True, capture_output=True,
).stdout ).stdout
lines = output.strip().split("\n") lines = output.strip().split("\n")
remote_list = [] remote_list = []
if lines[0] != '': if lines[0] != '':
for line in lines: for line in lines:
line = line.split("\t") line = line.split("\t")
remote_list.append(Remote(name=line[0], title=line[1], disabled=(len(line) == 3) and "disabled" in line[2])) remote_list.append(Remote(name=line[0], title=line[1], disabled=(len(line) == 3) and "disabled" in line[2]))
this.remotes[installation] = remote_list this.remotes[installation] = remote_list
# Masks # Masks
cmd = ['flatpak-spawn', '--host', cmd = ['flatpak-spawn', '--host',
'flatpak', 'mask',] 'flatpak', 'mask',]
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
output = subprocess.run( output = subprocess.run(
cmd, text=True, cmd, text=True,
capture_output=True, capture_output=True,
).stdout ).stdout
lines = output.strip().replace(" ", "").split("\n") lines = output.strip().replace(" ", "").split("\n")
if lines[0] != '': if lines[0] != '':
this.masks[installation] = lines this.masks[installation] = lines
# Pins # Pins
cmd = ['flatpak-spawn', '--host', cmd = ['flatpak-spawn', '--host',
'flatpak', 'pin',] 'flatpak', 'pin',]
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
output = subprocess.run( output = subprocess.run(
cmd, text=True, cmd, text=True,
capture_output=True, capture_output=True,
).stdout ).stdout
lines = output.strip().replace(" ", "").split("\n") lines = output.strip().replace(" ", "").split("\n")
if lines[0] != '': if lines[0] != '':
this.pins[installation] = lines this.pins[installation] = lines
try: try:
# Installations # Installations
# Get all config files for any extra installations # Get all config files for any extra installations
custom_install_config_path = "/run/host/etc/flatpak/installations.d" custom_install_config_path = "/run/host/etc/flatpak/installations.d"
if os.path.exists(custom_install_config_path): if os.path.exists(custom_install_config_path):
for file in os.listdir(custom_install_config_path): for file in os.listdir(custom_install_config_path):
with open(f"{custom_install_config_path}/{file}", "r") as f: with open(f"{custom_install_config_path}/{file}", "r") as f:
for line in f: for line in f:
if line.startswith("[Installation"): if line.startswith("[Installation"):
# Get specifically the installation name itself # Get specifically the installation name itself
this.installations.append(line.replace("[Installation \"", "").replace("\"]", "").strip()) this.installations.append(line.replace("[Installation \"", "").replace("\"]", "").strip())
this.installations.append("user") this.installations.append("user")
this.installations.append("system") this.installations.append("system")
for i in this.installations: for i in this.installations:
remote_info(i) remote_info(i)
remote_info("user") remote_info("user")
remote_info("system") remote_info("system")
# Packages # Packages
output = subprocess.run( output = subprocess.run(
['flatpak-spawn', '--host', 'flatpak', 'list', ['flatpak-spawn', '--host', 'flatpak', 'list',
'--columns=name,application,version,branch,arch,origin,installation,ref,size,options'], '--columns=name,application,version,branch,arch,origin,installation,ref,size,options'],
text=True, check=True, text=True, check=True,
capture_output=True, capture_output=True,
).stdout ).stdout
lines = output.strip().split("\n") lines = output.strip().split("\n")
for i in lines: for i in lines:
package = Flatpak(i.split("\t")) package = Flatpak(i.split("\t"))
this.flatpaks.append(package) this.flatpaks.append(package)
this.id_to_flatpak[package.info["id"]] = package this.id_to_flatpak[package.info["id"]] = package
this.ref_to_flatpak[package.info["ref"]] = package this.ref_to_flatpak[package.info["ref"]] = package
# Dependent Runtimes # Dependent Runtimes
output = subprocess.run( output = subprocess.run(
['flatpak-spawn', '--host', ['flatpak-spawn', '--host',
'flatpak', 'list', '--columns=runtime,ref'], 'flatpak', 'list', '--columns=runtime,ref'],
text=True, check=True, text=True, check=True,
capture_output=True, capture_output=True,
).stdout ).stdout
lines = output.split("\n") lines = output.split("\n")
for index, line in enumerate(lines): for index, line in enumerate(lines):
split_line = line.split("\t") split_line = line.split("\t")
if len(split_line) < 2 or split_line[0] == '': if len(split_line) < 2 or split_line[0] == '':
continue continue
package = this.flatpaks[index] package = this.flatpaks[index]
if package.is_runtime: if package.is_runtime:
continue continue
runtime = split_line[0] runtime = split_line[0]
package.dependent_runtime = this.ref_to_flatpak[runtime] package.dependent_runtime = this.ref_to_flatpak[runtime]
if not runtime in this.dependent_runtime_refs: if not runtime in this.dependent_runtime_refs:
this.dependent_runtime_refs.append(runtime) this.dependent_runtime_refs.append(runtime)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), cpe.stderr).toast) this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), cpe.stderr).toast)
except Exception as e: except Exception as e:
this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), str(e)).toast) this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), str(e)).toast)
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)

View File

@@ -2,117 +2,117 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $FiltersPage : Adw.NavigationPage { template $FiltersPage : Adw.NavigationPage {
title: _("Filter Packages"); title: _("Filter Packages");
Adw.ToolbarView { Adw.ToolbarView {
[top] [top]
Adw.HeaderBar {} Adw.HeaderBar {}
ScrolledWindow { ScrolledWindow {
Adw.Clamp { Adw.Clamp {
Box { Box {
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-top: 12; margin-top: 12;
margin-bottom: 12; margin-bottom: 12;
spacing: 24; spacing: 24;
orientation: vertical; orientation: vertical;
halign: fill; halign: fill;
Adw.PreferencesGroup { Adw.PreferencesGroup {
title: _("Filter by Package Type"); title: _("Filter by Package Type");
description: _("Show packages of these types"); description: _("Show packages of these types");
Adw.ActionRow application_row { Adw.ActionRow application_row {
title: _("Applications"); title: _("Applications");
subtitle: _("Packages that can be opened"); subtitle: _("Packages that can be opened");
CheckButton app_check {} CheckButton app_check {}
activatable-widget: app_check; activatable-widget: app_check;
} }
Adw.ActionRow runtime_row { Adw.ActionRow runtime_row {
title: _("Runtimes"); title: _("Runtimes");
subtitle: _("Packages that applications depend on"); subtitle: _("Packages that applications depend on");
CheckButton runtime_check {} CheckButton runtime_check {}
activatable-widget: runtime_check; activatable-widget: runtime_check;
} }
} }
Adw.PreferencesGroup remotes_group { Adw.PreferencesGroup remotes_group {
title: _("Filter by Remotes"); title: _("Filter by Remotes");
description: _("Show packages from selected remotes"); description: _("Show packages from selected remotes");
header-suffix: header-suffix:
Switch all_remotes_switch { Switch all_remotes_switch {
valign: center; valign: center;
} }
; ;
Adw.ActionRow { Adw.ActionRow {
visible: bind all_remotes_switch.active inverted; visible: bind all_remotes_switch.active inverted;
[child] [child]
Box { Box {
spacing: 3; spacing: 3;
orientation: vertical; orientation: vertical;
Label { Label {
margin-top: 7; margin-top: 7;
label: _("Showing packages from all remotes"); label: _("Showing packages from all remotes");
wrap: true; wrap: true;
halign: center; halign: center;
styles ["heading"] styles ["heading"]
} }
Label { Label {
label: _("Enable to show packages from selected remotes"); label: _("Enable to show packages from selected remotes");
margin-start: 16; margin-start: 16;
margin-end: 16; margin-end: 16;
margin-bottom: 8; margin-bottom: 8;
justify: center; justify: center;
halign: center; halign: center;
wrap: true; wrap: true;
} }
} }
} }
} }
Adw.PreferencesGroup runtimes_group { Adw.PreferencesGroup runtimes_group {
title: _("Filter by Runtimes"); title: _("Filter by Runtimes");
description: _("Show apps using selected runtimes"); description: _("Show apps using selected runtimes");
header-suffix: header-suffix:
Switch all_runtimes_switch { Switch all_runtimes_switch {
valign: center; valign: center;
} }
; ;
Adw.ActionRow { Adw.ActionRow {
visible: bind all_runtimes_switch.active inverted; visible: bind all_runtimes_switch.active inverted;
[child] [child]
Box { Box {
spacing: 3; spacing: 3;
orientation: vertical; orientation: vertical;
Label { Label {
margin-top: 7; margin-top: 7;
label: _("Showing apps using any runtime"); label: _("Showing apps using any runtime");
wrap: true; wrap: true;
halign: center; halign: center;
styles ["heading"] styles ["heading"]
} }
Label { Label {
label: _("Enable to show apps using selected runtimes"); label: _("Enable to show apps using selected runtimes");
margin-start: 16; margin-start: 16;
margin-end: 16; margin-end: 16;
margin-bottom: 8; margin-bottom: 8;
justify: center; justify: center;
halign: center; halign: center;
wrap: true; wrap: true;
} }
} }
} }
} }
} }
} }
} }
[bottom] [bottom]
ActionBar action_bar { ActionBar action_bar {
[center] [center]
Button reset_button { Button reset_button {
sensitive: bind action_bar.revealed; sensitive: bind action_bar.revealed;
margin-top: 3; margin-top: 3;
margin-bottom: 3; margin-bottom: 3;
label: _("Reset Filters"); label: _("Reset Filters");
styles ["pill"] styles ["pill"]
} }
} }
} }
} }

View File

@@ -2,195 +2,195 @@ from gi.repository import Adw, Gtk, Gio
from .host_info import HostInfo from .host_info import HostInfo
class FilterRow(Adw.ActionRow): class FilterRow(Adw.ActionRow):
__gtype_name__ = 'FilterRow' __gtype_name__ = 'FilterRow'
def __init__(self, item=None, installation=None, **kwargs): def __init__(self, item=None, installation=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.item = item self.item = item
self.installation = installation self.installation = installation
self.check_button = Gtk.CheckButton() self.check_button = Gtk.CheckButton()
self.add_suffix(self.check_button) self.add_suffix(self.check_button)
self.set_activatable_widget(self.check_button) self.set_activatable_widget(self.check_button)
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/filters_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/filters_page.ui")
class FiltersPage(Adw.NavigationPage): class FiltersPage(Adw.NavigationPage):
__gtype_name__ = 'FiltersPage' __gtype_name__ = 'FiltersPage'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
app_check = gtc() app_check = gtc()
runtime_check = gtc() runtime_check = gtc()
remotes_group = gtc() remotes_group = gtc()
all_remotes_switch = gtc() all_remotes_switch = gtc()
runtimes_group = gtc() runtimes_group = gtc()
all_runtimes_switch = gtc() all_runtimes_switch = gtc()
action_bar = gtc() action_bar = gtc()
reset_button = gtc() reset_button = gtc()
remote_rows = [] remote_rows = []
runtime_rows = [] runtime_rows = []
def reset_filters(self): def reset_filters(self):
self.settings.reset("show-apps") self.settings.reset("show-apps")
self.settings.reset("show-runtimes") self.settings.reset("show-runtimes")
self.settings.reset("remotes-list") self.settings.reset("remotes-list")
self.settings.reset("runtimes-list") self.settings.reset("runtimes-list")
self.generate_filters() self.generate_filters()
self.packages_page.apply_filters() self.packages_page.apply_filters()
def is_defaulted(self): def is_defaulted(self):
default = True default = True
if not self.app_check.get_active(): if not self.app_check.get_active():
default = False default = False
if self.runtime_check.get_active(): if self.runtime_check.get_active():
default = False default = False
if self.all_remotes_switch.get_active(): if self.all_remotes_switch.get_active():
default = False default = False
if self.all_runtimes_switch.get_active(): if self.all_runtimes_switch.get_active():
default = False default = False
self.action_bar.set_revealed(not default) self.action_bar.set_revealed(not default)
def update_gsettings(self): def update_gsettings(self):
self.is_defaulted() self.is_defaulted()
if not self.is_settings_settable: if not self.is_settings_settable:
return return
self.settings.set_boolean("show-apps", self.show_apps) self.settings.set_boolean("show-apps", self.show_apps)
self.settings.set_boolean("show-runtimes", self.show_runtimes) self.settings.set_boolean("show-runtimes", self.show_runtimes)
self.settings.set_string("remotes-list", self.remotes_string) self.settings.set_string("remotes-list", self.remotes_string)
self.settings.set_string("runtimes-list", self.runtimes_string) self.settings.set_string("runtimes-list", self.runtimes_string)
self.packages_page.apply_filters() self.packages_page.apply_filters()
def app_check_handler(self, *args): def app_check_handler(self, *args):
self.show_apps = self.app_check.get_active() self.show_apps = self.app_check.get_active()
self.update_gsettings() self.update_gsettings()
def runtime_check_handler(self, *args): def runtime_check_handler(self, *args):
self.show_runtimes = self.runtime_check.get_active() self.show_runtimes = self.runtime_check.get_active()
self.update_gsettings() self.update_gsettings()
def all_remotes_handler(self, switch, state): def all_remotes_handler(self, switch, state):
self.remotes_string = "" self.remotes_string = ""
if not state: if not state:
self.remotes_string = "all" self.remotes_string = "all"
for row in self.remote_rows: for row in self.remote_rows:
row.set_visible(state) row.set_visible(state)
if state and row.check_button.get_active(): if state and row.check_button.get_active():
self.remotes_string += f"{row.item.name}<>{row.installation};" self.remotes_string += f"{row.item.name}<>{row.installation};"
elif state: elif state:
self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "") self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "")
self.update_gsettings() self.update_gsettings()
def all_runtimes_handler(self, switch, state): def all_runtimes_handler(self, switch, state):
self.runtimes_string = "" self.runtimes_string = ""
if not state: if not state:
self.runtimes_string = "all" self.runtimes_string = "all"
for row in self.runtime_rows: for row in self.runtime_rows:
row.set_visible(state) row.set_visible(state)
if state and row.check_button.get_active(): if state and row.check_button.get_active():
self.runtimes_string += f"{row.item};" self.runtimes_string += f"{row.item};"
elif state: elif state:
self.runtimes_string.replace(f"{row.item};", "") self.runtimes_string.replace(f"{row.item};", "")
self.update_gsettings() self.update_gsettings()
def remote_row_check_handler(self, row): def remote_row_check_handler(self, row):
if row.check_button.get_active(): if row.check_button.get_active():
self.remotes_string += f"{row.item.name}<>{row.installation};" self.remotes_string += f"{row.item.name}<>{row.installation};"
else: else:
self.remotes_string = self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "") self.remotes_string = self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "")
self.update_gsettings() self.update_gsettings()
def runtime_row_check_handler(self, row): def runtime_row_check_handler(self, row):
if row.check_button.get_active(): if row.check_button.get_active():
self.runtimes_string += f"{row.item};" self.runtimes_string += f"{row.item};"
else: else:
self.runtimes_string = self.runtimes_string.replace(f"{row.item};", "") self.runtimes_string = self.runtimes_string.replace(f"{row.item};", "")
self.update_gsettings() self.update_gsettings()
def generate_remote_filters(self): def generate_remote_filters(self):
for row in self.remote_rows: for row in self.remote_rows:
self.remotes_group.remove(row) self.remotes_group.remove(row)
self.remote_rows.clear() self.remote_rows.clear()
for installation, remotes in HostInfo.remotes.items(): for installation, remotes in HostInfo.remotes.items():
for remote in remotes: for remote in remotes:
if remote.disabled: if remote.disabled:
continue continue
row = FilterRow(remote, installation) row = FilterRow(remote, installation)
row.set_title(remote.title) row.set_title(remote.title)
row.set_subtitle(_("Installation: {}").format(installation)) row.set_subtitle(_("Installation: {}").format(installation))
row.check_button.set_active(f"{remote.name}<>{installation}" in self.remotes_string) 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.check_button.connect("toggled", lambda *_, row=row: self.remote_row_check_handler(row))
row.set_visible(self.all_remotes_switch.get_active()) row.set_visible(self.all_remotes_switch.get_active())
self.remote_rows.append(row) self.remote_rows.append(row)
self.remotes_group.add(row) self.remotes_group.add(row)
self.remotes_group.set_visible(len(self.remote_rows) > 1) self.remotes_group.set_visible(len(self.remote_rows) > 1)
self.all_remotes_switch.set_active("all" != self.remotes_string) self.all_remotes_switch.set_active("all" != self.remotes_string)
def generate_runtime_filters(self): def generate_runtime_filters(self):
for row in self.runtime_rows: for row in self.runtime_rows:
self.runtimes_group.remove(row) self.runtimes_group.remove(row)
self.runtime_rows.clear() self.runtime_rows.clear()
if len(HostInfo.dependent_runtime_refs) < 2: if len(HostInfo.dependent_runtime_refs) < 2:
self.runtimes_group.set_visible(False) self.runtimes_group.set_visible(False)
if self.runtimes_string != "all": if self.runtimes_string != "all":
self.runtimes_string = "all" self.runtimes_string = "all"
self.settings.set_string("runtimes-list", self.runtimes_string) self.settings.set_string("runtimes-list", self.runtimes_string)
self.packages_page.apply_filters() self.packages_page.apply_filters()
return return
for j, ref in enumerate(HostInfo.dependent_runtime_refs): for j, ref in enumerate(HostInfo.dependent_runtime_refs):
row = FilterRow(ref) row = FilterRow(ref)
row.set_title(ref) row.set_title(ref)
row.check_button.set_active(ref in self.runtimes_string) 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.check_button.connect("toggled", lambda *_, row=row: self.runtime_row_check_handler(row))
row.set_visible(self.all_runtimes_switch.get_active()) row.set_visible(self.all_runtimes_switch.get_active())
self.runtime_rows.append(row) self.runtime_rows.append(row)
self.runtimes_group.add(row) self.runtimes_group.add(row)
self.runtimes_group.set_visible(len(self.runtime_rows) > 1) self.runtimes_group.set_visible(len(self.runtime_rows) > 1)
self.all_runtimes_switch.set_active("all" != self.runtimes_string) self.all_runtimes_switch.set_active("all" != self.runtimes_string)
def generate_filters(self): def generate_filters(self):
self.is_settings_settable = False self.is_settings_settable = False
self.show_apps = self.settings.get_boolean("show-apps") self.show_apps = self.settings.get_boolean("show-apps")
self.show_runtimes = self.settings.get_boolean("show-runtimes") self.show_runtimes = self.settings.get_boolean("show-runtimes")
self.remotes_string = self.settings.get_string("remotes-list") self.remotes_string = self.settings.get_string("remotes-list")
self.runtimes_string = self.settings.get_string("runtimes-list") self.runtimes_string = self.settings.get_string("runtimes-list")
self.app_check.set_active(self.show_apps) self.app_check.set_active(self.show_apps)
self.runtime_check.set_active(self.show_runtimes) self.runtime_check.set_active(self.show_runtimes)
self.generate_remote_filters() self.generate_remote_filters()
self.generate_runtime_filters() self.generate_runtime_filters()
self.is_settings_settable = True self.is_settings_settable = True
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Objects Creation # Extra Objects Creation
self.packages_page = None # To be set in packages page self.packages_page = None # To be set in packages page
self.main_window = HostInfo.main_window self.main_window = HostInfo.main_window
self.settings = Gio.Settings.new("io.github.flattool.Warehouse.filter") self.settings = Gio.Settings.new("io.github.flattool.Warehouse.filter")
self.is_settings_settable = False self.is_settings_settable = False
self.show_apps = self.settings.get_boolean("show-apps") self.show_apps = self.settings.get_boolean("show-apps")
self.show_runtimes = self.settings.get_boolean("show-runtimes") self.show_runtimes = self.settings.get_boolean("show-runtimes")
self.remotes_string = self.settings.get_string("remotes-list") self.remotes_string = self.settings.get_string("remotes-list")
self.runtimes_string = self.settings.get_string("runtimes-list") self.runtimes_string = self.settings.get_string("runtimes-list")
# Apply # Apply
if "," in self.runtimes_string: if "," in self.runtimes_string:
# Convert Warehouse 1.X runtimes filter string from , to ; for item seperationg # Convert Warehouse 1.X runtimes filter string from , to ; for item seperationg
self.runtimes_string = self.runtimes_string.replace(",", ";") self.runtimes_string = self.runtimes_string.replace(",", ";")
self.settings.set_string("runtimes-list", self.runtimes_string) self.settings.set_string("runtimes-list", self.runtimes_string)
# Connections # Connections
self.app_check.connect("toggled", self.app_check_handler) self.app_check.connect("toggled", self.app_check_handler)
self.runtime_check.connect("toggled", self.runtime_check_handler) self.runtime_check.connect("toggled", self.runtime_check_handler)
self.all_remotes_switch.connect("state-set", self.all_remotes_handler) self.all_remotes_switch.connect("state-set", self.all_remotes_handler)
self.all_runtimes_switch.connect("state-set", self.all_runtimes_handler) self.all_runtimes_switch.connect("state-set", self.all_runtimes_handler)
self.reset_button.connect("clicked", lambda *_: self.reset_filters()) self.reset_button.connect("clicked", lambda *_: self.reset_filters())

View File

@@ -2,184 +2,184 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $PackagesPage : Adw.BreakpointBin { template $PackagesPage : Adw.BreakpointBin {
width-request: 1; width-request: 1;
height-request: 1; height-request: 1;
Adw.Breakpoint packages_bpt { Adw.Breakpoint packages_bpt {
condition ("max-width: 600") condition ("max-width: 600")
setters { setters {
packages_split.collapsed: true; packages_split.collapsed: true;
packages_split.show-content: false; packages_split.show-content: false;
content_stack.transition-duration: 9999999; content_stack.transition-duration: 9999999;
reset_filters_button.visible: true; reset_filters_button.visible: true;
} }
} }
Adw.NavigationPage { Adw.NavigationPage {
title: _("Packages"); title: _("Packages");
Stack stack { Stack stack {
Adw.ToolbarView loading_view { Adw.ToolbarView loading_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.ToolbarView uninstalling_view { Adw.ToolbarView uninstalling_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.ToolbarView reinstalling_view { Adw.ToolbarView reinstalling_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.ToolbarView changing_version_view { Adw.ToolbarView changing_version_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.NavigationSplitView packages_split { Adw.NavigationSplitView packages_split {
sidebar-width-fraction: 0.5; sidebar-width-fraction: 0.5;
max-sidebar-width: 999999999; max-sidebar-width: 999999999;
sidebar: sidebar:
Adw.NavigationPage packages_navpage { Adw.NavigationPage packages_navpage {
title: _("Packages"); title: _("Packages");
Adw.ToastOverlay packages_toast_overlay { Adw.ToastOverlay packages_toast_overlay {
Adw.ToolbarView packages_tbv { Adw.ToolbarView packages_tbv {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
[start] [start]
ToggleButton search_button { ToggleButton search_button {
icon-name: "loupe-large-symbolic"; icon-name: "loupe-large-symbolic";
tooltip-text: _("Search Packages"); tooltip-text: _("Search Packages");
} }
[end] [end]
ToggleButton filter_button { ToggleButton filter_button {
icon-name: "funnel-symbolic"; icon-name: "funnel-symbolic";
tooltip-text: _("Filter Packages"); tooltip-text: _("Filter Packages");
} }
[end] [end]
ToggleButton select_button { ToggleButton select_button {
icon-name: "selection-mode-symbolic"; icon-name: "selection-mode-symbolic";
tooltip-text: _("Select Packages"); tooltip-text: _("Select Packages");
} }
} }
[top] [top]
SearchBar search_bar { SearchBar search_bar {
search-mode-enabled: bind search_button.active bidirectional; search-mode-enabled: bind search_button.active bidirectional;
SearchEntry search_entry { SearchEntry search_entry {
hexpand: true; hexpand: true;
placeholder-text: _("Search Packages"); placeholder-text: _("Search Packages");
} }
} }
Stack status_stack { Stack status_stack {
ScrolledWindow scrolled_window { ScrolledWindow scrolled_window {
ListBox packages_list_box { ListBox packages_list_box {
styles ["navigation-sidebar"] styles ["navigation-sidebar"]
} }
} }
Adw.StatusPage no_filter_results { Adw.StatusPage no_filter_results {
title: _("No Packages Match Filters"); title: _("No Packages Match Filters");
description: _("No installed package matches all of the currently applied filters"); description: _("No installed package matches all of the currently applied filters");
icon-name: "funnel-symbolic"; icon-name: "funnel-symbolic";
Button reset_filters_button { Button reset_filters_button {
label: _("Reset Filters"); label: _("Reset Filters");
halign: center; halign: center;
visible: false; visible: false;
styles ["pill"] styles ["pill"]
} }
} }
Adw.StatusPage no_packages { Adw.StatusPage no_packages {
title: _("No Packages Found"); title: _("No Packages Found");
description: _("Warehouse cannot see the list of installed packages or your system has no packages installed"); description: _("Warehouse cannot see the list of installed packages or your system has no packages installed");
icon-name: "error-symbolic"; icon-name: "error-symbolic";
} }
Adw.StatusPage no_results { Adw.StatusPage no_results {
title: _("No Results Found"); title: _("No Results Found");
description: _("Try a different search"); description: _("Try a different search");
icon-name: "system-search-symbolic"; icon-name: "system-search-symbolic";
} }
} }
[bottom] [bottom]
Revealer { Revealer {
reveal-child: bind select_button.active; reveal-child: bind select_button.active;
transition-type: slide_up; transition-type: slide_up;
[center] [center]
Box bottom_bar { Box bottom_bar {
styles ["toolbar"] styles ["toolbar"]
hexpand: true; hexpand: true;
homogeneous: true; homogeneous: true;
Button select_all_button { Button select_all_button {
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "selection-mode-symbolic"; icon-name: "selection-mode-symbolic";
label: _("Select All"); label: _("Select All");
can-shrink: true; can-shrink: true;
} }
} }
MenuButton copy_button { MenuButton copy_button {
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "edit-copy-symbolic"; icon-name: "edit-copy-symbolic";
label: _("Copy"); label: _("Copy");
can-shrink: true; can-shrink: true;
} }
popover: copy_pop; popover: copy_pop;
} }
Button uninstall_button { Button uninstall_button {
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "user-trash-symbolic"; icon-name: "user-trash-symbolic";
label: _("Uninstall"); label: _("Uninstall");
can-shrink: true; can-shrink: true;
} }
} }
} }
} }
} }
} }
} }
; ;
content: content:
Adw.NavigationPage { Adw.NavigationPage {
title: "Content Stack"; title: "Content Stack";
Stack content_stack { Stack content_stack {
transition-type: slide_left_right; transition-type: slide_left_right;
$PropertiesPage properties_page {} $PropertiesPage properties_page {}
$FiltersPage filters_page {} $FiltersPage filters_page {}
} }
} }
; ;
} }
} }
} }
} }
Popover copy_pop { Popover copy_pop {
styles ["menu"] styles ["menu"]
ListBox copy_menu { ListBox copy_menu {
Label copy_names { Label copy_names {
label: _("Copy Names"); label: _("Copy Names");
halign: start; halign: start;
} }
Label copy_ids { Label copy_ids {
label: _("Copy IDs"); label: _("Copy IDs");
halign: start; halign: start;
} }
Label copy_refs { Label copy_refs {
label: _("Copy Refs"); label: _("Copy Refs");
halign: start; halign: start;
} }
} }
} }

View File

@@ -13,388 +13,388 @@ import subprocess, os
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/packages_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/packages_page.ui")
class PackagesPage(Adw.BreakpointBin): class PackagesPage(Adw.BreakpointBin):
__gtype_name__ = 'PackagesPage' __gtype_name__ = 'PackagesPage'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
packages_bpt = gtc() packages_bpt = gtc()
packages_toast_overlay = gtc() packages_toast_overlay = gtc()
stack = gtc() stack = gtc()
status_stack = gtc() status_stack = gtc()
scrolled_window = gtc() scrolled_window = gtc()
loading_view = gtc() loading_view = gtc()
uninstalling_view = gtc() uninstalling_view = gtc()
reinstalling_view = gtc() reinstalling_view = gtc()
changing_version_view = gtc() changing_version_view = gtc()
no_filter_results = gtc() no_filter_results = gtc()
reset_filters_button = gtc() reset_filters_button = gtc()
no_packages = gtc() no_packages = gtc()
no_results = gtc() no_results = gtc()
filter_button = gtc() filter_button = gtc()
search_button = gtc() search_button = gtc()
search_bar = gtc() search_bar = gtc()
search_entry = gtc() search_entry = gtc()
packages_split = gtc() packages_split = gtc()
packages_list_box = gtc() packages_list_box = gtc()
select_button = gtc() select_button = gtc()
packages_navpage = gtc() packages_navpage = gtc()
select_all_button = gtc() select_all_button = gtc()
content_stack = gtc() content_stack = gtc()
copy_button = gtc() copy_button = gtc()
copy_pop = gtc() copy_pop = gtc()
copy_menu = gtc() copy_menu = gtc()
copy_names = gtc() copy_names = gtc()
copy_ids = gtc() copy_ids = gtc()
copy_refs = gtc() copy_refs = gtc()
uninstall_button = gtc() uninstall_button = gtc()
properties_page = gtc() properties_page = gtc()
filters_page = gtc() filters_page = gtc()
# Referred to in the main window # Referred to in the main window
# It is used to determine if a new page should be made or not # It is used to determine if a new page should be made or not
# This must be set to the created object from within the class's __init__ method # This must be set to the created object from within the class's __init__ method
instance = None instance = None
page_name = "packages" page_name = "packages"
last_activated_row = None last_activated_row = None
def set_status(self, to_set): def set_status(self, to_set):
if to_set is self.scrolled_window: if to_set is self.scrolled_window:
self.properties_page.stack.set_visible_child(self.properties_page.nav_view) self.properties_page.stack.set_visible_child(self.properties_page.nav_view)
self.select_button.set_sensitive(True) self.select_button.set_sensitive(True)
self.filter_button.set_sensitive(True) self.filter_button.set_sensitive(True)
self.filters_page.set_sensitive(True) self.filters_page.set_sensitive(True)
self.search_button.set_sensitive(True) self.search_button.set_sensitive(True)
self.search_entry.set_editable(True) self.search_entry.set_editable(True)
else: else:
self.select_button.set_sensitive(False) self.select_button.set_sensitive(False)
if to_set is self.no_packages: if to_set is self.no_packages:
self.properties_page.stack.set_visible_child(self.properties_page.error_tbv) self.properties_page.stack.set_visible_child(self.properties_page.error_tbv)
self.filter_button.set_sensitive(False) self.filter_button.set_sensitive(False)
self.filter_button.set_active(False) self.filter_button.set_active(False)
if to_set is self.no_filter_results: if to_set is self.no_filter_results:
self.properties_page.stack.set_visible_child(self.properties_page.error_tbv) self.properties_page.stack.set_visible_child(self.properties_page.error_tbv)
self.filter_button.set_sensitive(True) self.filter_button.set_sensitive(True)
self.filters_page.set_sensitive(True) self.filters_page.set_sensitive(True)
if not self.packages_split.get_collapsed(): if not self.packages_split.get_collapsed():
self.filter_button.set_active(True) self.filter_button.set_active(True)
if to_set is self.no_results: if to_set is self.no_results:
self.filters_page.set_sensitive(False) self.filters_page.set_sensitive(False)
if to_set is self.loading_packages: if to_set is self.loading_packages:
self.stack.set_visible_child(self.loading_view) self.stack.set_visible_child(self.loading_view)
elif to_set is self.uninstalling: elif to_set is self.uninstalling:
self.stack.set_visible_child(self.uninstalling_view) self.stack.set_visible_child(self.uninstalling_view)
elif to_set is self.reinstalling: elif to_set is self.reinstalling:
self.stack.set_visible_child(self.reinstalling_view) self.stack.set_visible_child(self.reinstalling_view)
elif to_set is self.changing_version: elif to_set is self.changing_version:
self.stack.set_visible_child(self.changing_version_view) self.stack.set_visible_child(self.changing_version_view)
else: else:
self.stack.set_visible_child(self.packages_split) self.stack.set_visible_child(self.packages_split)
self.status_stack.set_visible_child(to_set) self.status_stack.set_visible_child(to_set)
def apply_filters(self): def apply_filters(self):
i = 0 i = 0
show_apps = self.filter_settings.get_boolean("show-apps") show_apps = self.filter_settings.get_boolean("show-apps")
show_runtimes = self.filter_settings.get_boolean("show-runtimes") show_runtimes = self.filter_settings.get_boolean("show-runtimes")
remotes_list = self.filter_settings.get_string("remotes-list") remotes_list = self.filter_settings.get_string("remotes-list")
runtimes_list = self.filter_settings.get_string("runtimes-list") runtimes_list = self.filter_settings.get_string("runtimes-list")
total_visible = 0 total_visible = 0
while row := self.packages_list_box.get_row_at_index(i): while row := self.packages_list_box.get_row_at_index(i):
i += 1 i += 1
visible = True visible = True
if row.package.is_runtime and not show_runtimes: if row.package.is_runtime and not show_runtimes:
visible = False visible = False
if (not row.package.is_runtime) and (not show_apps): if (not row.package.is_runtime) and (not show_apps):
visible = False visible = False
if remotes_list != "all" and not f"{row.package.info['origin']}<>{row.package.info['installation']}" in remotes_list: if remotes_list != "all" and not f"{row.package.info['origin']}<>{row.package.info['installation']}" in remotes_list:
visible = False visible = False
if runtimes_list != "all" and (row.package.is_runtime or row.package.dependent_runtime and not row.package.dependent_runtime.info["ref"] in runtimes_list): if runtimes_list != "all" and (row.package.is_runtime or row.package.dependent_runtime and not row.package.dependent_runtime.info["ref"] in runtimes_list):
visible = False visible = False
row.set_visible(visible) row.set_visible(visible)
if visible: if visible:
total_visible += 1 total_visible += 1
else: else:
row.check_button.set_active(False) row.check_button.set_active(False)
if total_visible == 0: if total_visible == 0:
self.set_status(self.no_filter_results) self.set_status(self.no_filter_results)
else: else:
GLib.idle_add(lambda *_: self.set_status(self.scrolled_window)) GLib.idle_add(lambda *_: self.set_status(self.scrolled_window))
if self.current_row_for_properties and not self.current_row_for_properties.get_visible(): if self.current_row_for_properties and not self.current_row_for_properties.get_visible():
self.select_first_visible_row() self.select_first_visible_row()
def select_first_visible_row(self): def select_first_visible_row(self):
first_visible_row = None first_visible_row = None
i = 0 i = 0
while row := self.packages_list_box.get_row_at_index(i): while row := self.packages_list_box.get_row_at_index(i):
i += 1 i += 1
if row.get_visible(): if row.get_visible():
first_visible_row = row first_visible_row = row
self.current_row_for_properties = row self.current_row_for_properties = row
break break
if not first_visible_row is None: if not first_visible_row is None:
self.packages_list_box.select_row(first_visible_row) self.packages_list_box.select_row(first_visible_row)
self.properties_page.set_properties(first_visible_row.package) self.properties_page.set_properties(first_visible_row.package)
def row_select_handler(self, row): def row_select_handler(self, row):
if row.check_button.get_active(): if row.check_button.get_active():
self.selected_rows.append(row) self.selected_rows.append(row)
else: else:
self.selected_rows.remove(row) self.selected_rows.remove(row)
if (total := len(self.selected_rows)) > 0: if (total := len(self.selected_rows)) > 0:
self.packages_navpage.set_title(_("{} Selected").format(total)) self.packages_navpage.set_title(_("{} Selected").format(total))
self.copy_button.set_sensitive(True) self.copy_button.set_sensitive(True)
self.uninstall_button.set_sensitive(True) self.uninstall_button.set_sensitive(True)
else: else:
self.packages_navpage.set_title(_("Packages")) self.packages_navpage.set_title(_("Packages"))
self.copy_button.set_sensitive(False) self.copy_button.set_sensitive(False)
self.uninstall_button.set_sensitive(False) self.uninstall_button.set_sensitive(False)
def select_all_handler(self, *args): def select_all_handler(self, *args):
i = 0 i = 0
while row := self.packages_list_box.get_row_at_index(i): while row := self.packages_list_box.get_row_at_index(i):
i += 1 i += 1
row.check_button.set_active(row.get_visible()) row.check_button.set_active(row.get_visible())
def row_rclick_handler(self, row): def row_rclick_handler(self, row):
self.select_button.set_active(True) self.select_button.set_active(True)
GLib.idle_add(lambda *_, button=row.check_button: button.set_active(not button.get_active())) GLib.idle_add(lambda *_, button=row.check_button: button.set_active(not button.get_active()))
def generate_list(self, *args): def generate_list(self, *args):
self.properties_page.nav_view.pop_to_page(self.properties_page.inner_nav_page) self.properties_page.nav_view.pop_to_page(self.properties_page.inner_nav_page)
self.packages_list_box.remove_all() self.packages_list_box.remove_all()
self.selected_rows.clear() self.selected_rows.clear()
GLib.idle_add(lambda *_: self.filters_page.generate_filters()) GLib.idle_add(lambda *_: self.filters_page.generate_filters())
self.copy_button.set_sensitive(False) self.copy_button.set_sensitive(False)
self.uninstall_button.set_sensitive(False) self.uninstall_button.set_sensitive(False)
if len(HostInfo.flatpaks) == 0: if len(HostInfo.flatpaks) == 0:
self.set_status(self.no_packages) self.set_status(self.no_packages)
return return
for package in HostInfo.flatpaks: for package in HostInfo.flatpaks:
row = AppRow(package, self.row_rclick_handler) row = AppRow(package, self.row_rclick_handler)
package.app_row = row package.app_row = row
row.masked_status_icon.set_visible(package.is_masked) row.masked_status_icon.set_visible(package.is_masked)
row.pinned_status_icon.set_visible(package.is_pinned) row.pinned_status_icon.set_visible(package.is_pinned)
row.eol_package_package_status_icon.set_visible(package.is_eol) row.eol_package_package_status_icon.set_visible(package.is_eol)
row.check_button.set_visible(self.select_button.get_active()) row.check_button.set_visible(self.select_button.get_active())
row.check_button.connect("toggled", lambda *_, row=row: self.row_select_handler(row)) row.check_button.connect("toggled", lambda *_, row=row: self.row_select_handler(row))
try: try:
if not package.is_runtime: if not package.is_runtime:
row.eol_runtime_status_icon.set_visible(package.dependent_runtime.is_eol) row.eol_runtime_status_icon.set_visible(package.dependent_runtime.is_eol)
except Exception as e: except Exception as e:
self.packages_toast_overlay.add_toast(ErrorToast(_("Error getting Flatpak '{}'").format(package.info["name"]), str(e)).toast) self.packages_toast_overlay.add_toast(ErrorToast(_("Error getting Flatpak '{}'").format(package.info["name"]), str(e)).toast)
self.packages_list_box.append(row) self.packages_list_box.append(row)
self.apply_filters() self.apply_filters()
self.select_first_visible_row() self.select_first_visible_row()
self.scrolled_window.set_vadjustment(Gtk.Adjustment.new(0,0,0,0,0,0)) # Scroll list to top self.scrolled_window.set_vadjustment(Gtk.Adjustment.new(0,0,0,0,0,0)) # Scroll list to top
def row_activate_handler(self, list_box, row): def row_activate_handler(self, list_box, row):
if self.select_button.get_active(): if self.select_button.get_active():
row.check_button.set_active(not row.check_button.get_active()) row.check_button.set_active(not row.check_button.get_active())
return return
self.last_activated_row = row self.last_activated_row = row
self.properties_page.set_properties(row.package) self.properties_page.set_properties(row.package)
self.properties_page.nav_view.pop() self.properties_page.nav_view.pop()
self.packages_split.set_show_content(True) self.packages_split.set_show_content(True)
self.filter_button.set_active(False) self.filter_button.set_active(False)
self.current_row_for_properties = row self.current_row_for_properties = row
def filter_func(self, row): def filter_func(self, row):
search_text = self.search_entry.get_text().lower() search_text = self.search_entry.get_text().lower()
title = row.get_title().lower() title = row.get_title().lower()
subtitle = row.get_subtitle().lower() subtitle = row.get_subtitle().lower()
if row.get_visible() and (search_text in title or search_text in subtitle): if row.get_visible() and (search_text in title or search_text in subtitle):
self.is_result = True self.is_result = True
return True return True
def set_selection_mode(self, is_enabled): def set_selection_mode(self, is_enabled):
if is_enabled: if is_enabled:
self.packages_list_box.set_selection_mode(Gtk.SelectionMode.NONE) self.packages_list_box.set_selection_mode(Gtk.SelectionMode.NONE)
else: else:
self.packages_list_box.set_selection_mode(Gtk.SelectionMode.SINGLE) self.packages_list_box.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.packages_list_box.select_row(self.last_activated_row) self.packages_list_box.select_row(self.last_activated_row)
i = 0 i = 0
while row := self.packages_list_box.get_row_at_index(i): while row := self.packages_list_box.get_row_at_index(i):
i += 1 i += 1
GLib.idle_add(row.check_button.set_active, False) GLib.idle_add(row.check_button.set_active, False)
GLib.idle_add(row.check_button.set_visible, is_enabled) GLib.idle_add(row.check_button.set_visible, is_enabled)
def selection_copy(self, box, row): def selection_copy(self, box, row):
self.copy_pop.popdown() self.copy_pop.popdown()
info = "" info = ""
feedback = "" feedback = ""
match row.get_child(): match row.get_child():
case self.copy_names: case self.copy_names:
info = "name" info = "name"
feedback = _("Names") feedback = _("Names")
case self.copy_ids: case self.copy_ids:
info = "id" info = "id"
feedback = _("IDs") feedback = _("IDs")
case self.copy_refs: case self.copy_refs:
info = "ref" info = "ref"
feedback = _("Refs") feedback = _("Refs")
to_copy = [] to_copy = []
for row in self.selected_rows: for row in self.selected_rows:
to_copy.append(row.package.info[info]) to_copy.append(row.package.info[info])
to_copy += ['\n'] to_copy += ['\n']
try: try:
HostInfo.clipboard.set("".join(to_copy[:-1])) HostInfo.clipboard.set("".join(to_copy[:-1]))
self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(feedback))) self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(feedback)))
except Exception as e: except Exception as e:
self.packages_toast_overlay.add_toast(ErrorToast(_("Could not copy {}").format(feedback), str(e)).toast) self.packages_toast_overlay.add_toast(ErrorToast(_("Could not copy {}").format(feedback), str(e)).toast)
def selection_uninstall(self, *args): def selection_uninstall(self, *args):
if len(self.selected_rows) < 1 or not self.uninstall_button.get_sensitive(): if len(self.selected_rows) < 1 or not self.uninstall_button.get_sensitive():
return return
def on_response(should_trash): def on_response(should_trash):
GLib.idle_add(lambda *_: self.set_status(self.uninstalling)) GLib.idle_add(lambda *_: self.set_status(self.uninstalling))
error = [] error = []
def thread(*args): def thread(*args):
HostInfo.main_window.add_refresh_lockout("batch uninstalling packages") HostInfo.main_window.add_refresh_lockout("batch uninstalling packages")
cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y'] cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y']
to_uninstall = {} # { <remote><><installation>: [<ref1>, <ref2>, <ref3>, ...], ... } to_uninstall = {} # { <remote><><installation>: [<ref1>, <ref2>, <ref3>, ...], ... }
to_trash = [] to_trash = []
for row in self.selected_rows: for row in self.selected_rows:
key = row.package.info['installation'] key = row.package.info['installation']
if ls := to_uninstall.get(key, False): if ls := to_uninstall.get(key, False):
ls.append(row.package.info['ref']) ls.append(row.package.info['ref'])
else: else:
to_uninstall[key] = [row.package.info['ref']] to_uninstall[key] = [row.package.info['ref']]
if should_trash and os.path.exists(row.package.data_path): if should_trash and os.path.exists(row.package.data_path):
to_trash.append(row.package.data_path) to_trash.append(row.package.data_path)
for installation, packages in to_uninstall.items(): for installation, packages in to_uninstall.items():
suffix = [] suffix = []
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
suffix.append(f"--{installation}") suffix.append(f"--{installation}")
else: else:
suffix.append(f"--installation={installation}") suffix.append(f"--installation={installation}")
try: try:
subprocess.run(cmd + suffix + packages, check=True, text=True, capture_output=True) subprocess.run(cmd + suffix + packages, check=True, text=True, capture_output=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error.append(str(cpe.stderr)) error.append(str(cpe.stderr))
except Exception as e: except Exception as e:
error.append(str(e)) error.append(str(e))
if should_trash and len(to_trash) > 0: if should_trash and len(to_trash) > 0:
try: try:
subprocess.run(['gio', 'trash'] + to_trash, check=True, text=True, capture_output=True) subprocess.run(['gio', 'trash'] + to_trash, check=True, text=True, capture_output=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error.append(cpe) error.append(cpe)
def callback(*args): def callback(*args):
self.main_window.refresh_handler() self.main_window.refresh_handler()
HostInfo.main_window.remove_refresh_lockout("batch uninstalling packages") HostInfo.main_window.remove_refresh_lockout("batch uninstalling packages")
if len(error) > 0: if len(error) > 0:
details = "\n\n".join(error) details = "\n\n".join(error)
GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(ErrorToast(_("Errors occurred while uninstalling"), details).toast)) GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(ErrorToast(_("Errors occurred while uninstalling"), details).toast))
else: else:
GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled Packages")))) GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled Packages"))))
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
dialog = UninstallDialog(on_response, True) dialog = UninstallDialog(on_response, True)
dialog.present(self.main_window) dialog.present(self.main_window)
def start_loading(self): def start_loading(self):
self.search_button.set_active(False) self.search_button.set_active(False)
self.last_activated_row = None self.last_activated_row = None
self.packages_navpage.set_title(_("Packages")) self.packages_navpage.set_title(_("Packages"))
self.select_button.set_active(False) self.select_button.set_active(False)
self.set_status(self.loading_packages) self.set_status(self.loading_packages)
def end_loading(self): def end_loading(self):
GLib.idle_add(lambda *_: self.generate_list()) GLib.idle_add(lambda *_: self.generate_list())
def select_button_handler(self, button): def select_button_handler(self, button):
self.set_selection_mode(button.get_active()) self.set_selection_mode(button.get_active())
def filter_button_handler(self, button): def filter_button_handler(self, button):
if button.get_active(): if button.get_active():
self.content_stack.set_visible_child(self.filters_page) self.content_stack.set_visible_child(self.filters_page)
self.packages_split.set_show_content(True) self.packages_split.set_show_content(True)
else: else:
self.content_stack.set_visible_child(self.properties_page) self.content_stack.set_visible_child(self.properties_page)
self.packages_split.set_show_content(False) self.packages_split.set_show_content(False)
def filter_page_handler(self, *args): def filter_page_handler(self, *args):
if self.packages_split.get_collapsed() and not self.packages_split.get_show_content(): if self.packages_split.get_collapsed() and not self.packages_split.get_show_content():
self.filter_button.set_active(False) self.filter_button.set_active(False)
def on_invalidate(self, row): def on_invalidate(self, row):
current_status = self.status_stack.get_visible_child() current_status = self.status_stack.get_visible_child()
if not current_status is self.no_results: if not current_status is self.no_results:
self.prev_status = current_status self.prev_status = current_status
self.is_result = False self.is_result = False
self.packages_list_box.invalidate_filter() self.packages_list_box.invalidate_filter()
if self.is_result: if self.is_result:
self.set_status(self.prev_status) self.set_status(self.prev_status)
else: else:
self.set_status(self.no_results) self.set_status(self.no_results)
def sort_func(self, row1, row2): def sort_func(self, row1, row2):
return row1.package.info["name"].lower() > row2.package.info["name"].lower() return row1.package.info["name"].lower() > row2.package.info["name"].lower()
def on_escape_handler(self): def on_escape_handler(self):
if self.select_button.get_active(): if self.select_button.get_active():
self.select_button.set_active(False) self.select_button.set_active(False)
elif self.filter_button.get_active(): elif self.filter_button.get_active():
self.filter_button.set_active(False) self.filter_button.set_active(False)
def __init__(self, main_window, **kwargs): def __init__(self, main_window, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.main_window = main_window self.main_window = main_window
self.loading_packages = LoadingStatus(_("Loading Packages"), _("This should only take a moment")) self.loading_packages = LoadingStatus(_("Loading Packages"), _("This should only take a moment"))
self.uninstalling = LoadingStatus(_("Uninstalling 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.uninstalling_view.set_content(self.uninstalling)
self.reinstalling = LoadingStatus(_("Reinstalling Package"), _("This could take a while"), True, PackageInstallWorker.cancel) self.reinstalling = LoadingStatus(_("Reinstalling Package"), _("This could take a while"), True, PackageInstallWorker.cancel)
self.reinstalling_view.set_content(self.reinstalling) self.reinstalling_view.set_content(self.reinstalling)
self.changing_version = LoadingStatus(_("Changing Version"), _("This could take a while"), True, ChangeVersionWorker.cancel) self.changing_version = LoadingStatus(_("Changing Version"), _("This could take a while"), True, ChangeVersionWorker.cancel)
self.changing_version_view.set_content(self.changing_version) self.changing_version_view.set_content(self.changing_version)
self.filter_settings = Gio.Settings.new("io.github.flattool.Warehouse.filter") self.filter_settings = Gio.Settings.new("io.github.flattool.Warehouse.filter")
self.is_result = False self.is_result = False
self.prev_status = None self.prev_status = None
self.selected_rows = [] self.selected_rows = []
self.current_row_for_properties = None self.current_row_for_properties = None
self.on_backspace_handler = self.selection_uninstall self.on_backspace_handler = self.selection_uninstall
# Apply # Apply
self.loading_view.set_content(self.loading_packages) self.loading_view.set_content(self.loading_packages)
self.packages_list_box.set_filter_func(self.filter_func) self.packages_list_box.set_filter_func(self.filter_func)
self.packages_list_box.set_sort_func(self.sort_func) self.packages_list_box.set_sort_func(self.sort_func)
self.properties_page.packages_page = self self.properties_page.packages_page = self
self.filters_page.packages_page = self self.filters_page.packages_page = self
self.__class__.instance = self self.__class__.instance = self
# Connections # Connections
self.search_entry.connect("search-changed", self.on_invalidate) self.search_entry.connect("search-changed", self.on_invalidate)
self.search_bar.set_key_capture_widget(main_window) self.search_bar.set_key_capture_widget(main_window)
self.packages_list_box.connect("row-activated", self.row_activate_handler) self.packages_list_box.connect("row-activated", self.row_activate_handler)
self.select_button.connect("toggled", self.select_button_handler) self.select_button.connect("toggled", self.select_button_handler)
self.filter_button.connect("toggled", self.filter_button_handler) self.filter_button.connect("toggled", self.filter_button_handler)
self.reset_filters_button.connect("clicked", lambda *_: self.filters_page.reset_filters()) 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_split.connect("notify::show-content", self.filter_page_handler)
self.packages_bpt.connect("apply", self.filter_page_handler) self.packages_bpt.connect("apply", self.filter_page_handler)
self.select_all_button.connect("clicked", self.select_all_handler) self.select_all_button.connect("clicked", self.select_all_handler)
self.copy_menu.connect("row-activated", self.selection_copy) self.copy_menu.connect("row-activated", self.selection_copy)
self.uninstall_button.connect("clicked", self.selection_uninstall) self.uninstall_button.connect("clicked", self.selection_uninstall)

View File

@@ -2,26 +2,26 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $UninstallDialog : Adw.AlertDialog { template $UninstallDialog : Adw.AlertDialog {
extra-child: extra-child:
Adw.PreferencesGroup group { Adw.PreferencesGroup group {
Adw.ActionRow { Adw.ActionRow {
title: _("Keep"); title: _("Keep");
subtitle: _("Allows restoring app settings and content"); subtitle: _("Allows restoring app settings and content");
activatable-widget: keep; activatable-widget: keep;
[prefix] [prefix]
CheckButton keep { CheckButton keep {
active: true; active: true;
} }
} }
Adw.ActionRow { Adw.ActionRow {
title: _("Trash"); title: _("Trash");
subtitle: _("Send data to the trash"); subtitle: _("Send data to the trash");
activatable-widget: trash; activatable-widget: trash;
[prefix] [prefix]
CheckButton trash { CheckButton trash {
group: keep; group: keep;
} }
} }
} }
; ;
} }

View File

@@ -2,41 +2,41 @@ from gi.repository import Adw, Gtk, GLib
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/uninstall_dialog.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/packages_page/uninstall_dialog.ui")
class UninstallDialog(Adw.AlertDialog): class UninstallDialog(Adw.AlertDialog):
__gtype_name__ = "UninstallDialog" __gtype_name__ = "UninstallDialog"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
group = gtc() group = gtc()
trash = gtc() trash = gtc()
is_open = False is_open = False
def on_response(self, dialog, response): def on_response(self, dialog, response):
self.__class__.is_open = False self.__class__.is_open = False
if response != "continue": if response != "continue":
return return
self.continue_callback(self.trash.get_active()) self.continue_callback(self.trash.get_active())
def present(self, *args, **kwargs): def present(self, *args, **kwargs):
if self.__class__.is_open: if self.__class__.is_open:
return return
self.__class__.is_open = True self.__class__.is_open = True
super().present(*args, **kwargs) super().present(*args, **kwargs)
def __init__(self, continue_callback, show_trash_option, package_name=None, **kwargs): def __init__(self, continue_callback, show_trash_option, package_name=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
if package_name: if package_name:
self.set_heading(GLib.markup_escape_text(_("Uninstall {}?").format(package_name))) self.set_heading(GLib.markup_escape_text(_("Uninstall {}?").format(package_name)))
self.set_body(GLib.markup_escape_text(_("It will not be possible to use {} after removal").format(package_name))) self.set_body(GLib.markup_escape_text(_("It will not be possible to use {} after removal").format(package_name)))
else: else:
self.set_heading(GLib.markup_escape_text(_("Uninstall Packages?"))) self.set_heading(GLib.markup_escape_text(_("Uninstall Packages?")))
self.set_body(GLib.markup_escape_text(_("It will not be possible to use these packages after removal"))) self.set_body(GLib.markup_escape_text(_("It will not be possible to use these packages after removal")))
self.continue_callback = continue_callback self.continue_callback = continue_callback
self.add_response("cancel", _("Cancel")) self.add_response("cancel", _("Cancel"))
self.add_response("continue", _("Uninstall")) self.add_response("continue", _("Uninstall"))
self.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) self.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
self.connect("response", self.on_response) self.connect("response", self.on_response)
self.group.set_title(GLib.markup_escape_text(_("App Settings & Content"))) self.group.set_title(GLib.markup_escape_text(_("App Settings & Content")))
self.group.set_visible(show_trash_option) self.group.set_visible(show_trash_option)

View File

@@ -2,245 +2,245 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $PropertiesPage : Adw.NavigationPage { template $PropertiesPage : Adw.NavigationPage {
title: "Outer Page"; title: "Outer Page";
Stack stack { Stack stack {
Adw.ToolbarView loading_tbv { Adw.ToolbarView loading_tbv {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
show-title: false; show-title: false;
} }
} }
Adw.ToolbarView error_tbv { Adw.ToolbarView error_tbv {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
show-title: false; show-title: false;
} }
Adw.StatusPage { Adw.StatusPage {
title: _("Properties Page Unavailable"); title: _("Properties Page Unavailable");
description: _("Cannot show the properties page at this time"); description: _("Cannot show the properties page at this time");
icon-name: "error-symbolic"; icon-name: "error-symbolic";
} }
} }
Adw.NavigationView nav_view { Adw.NavigationView nav_view {
Adw.NavigationPage inner_nav_page { Adw.NavigationPage inner_nav_page {
title: "Inner Page"; title: "Inner Page";
Adw.ToastOverlay toast_overlay { Adw.ToastOverlay toast_overlay {
Adw.ToolbarView { Adw.ToolbarView {
[top] [top]
Adw.HeaderBar header_bar { Adw.HeaderBar header_bar {
show-title: false; show-title: false;
[end] [end]
MenuButton more_menu_button { MenuButton more_menu_button {
icon-name: "view-more-symbolic"; icon-name: "view-more-symbolic";
popover: more_menu; popover: more_menu;
} }
} }
ScrolledWindow scrolled_window { ScrolledWindow scrolled_window {
Adw.Clamp { Adw.Clamp {
Box { Box {
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-bottom: 12; margin-bottom: 12;
orientation: vertical; orientation: vertical;
halign: fill; halign: fill;
Image app_icon { Image app_icon {
pixel-size: 100; pixel-size: 100;
margin-top: 6; margin-top: 6;
margin-bottom: 18; margin-bottom: 18;
icon-name: "application-x-executable-symbolic"; icon-name: "application-x-executable-symbolic";
styles["icon-dropshadow"] styles["icon-dropshadow"]
} }
Label name { Label name {
styles ["title-1"] styles ["title-1"]
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
justify: center; justify: center;
margin-bottom: 12; margin-bottom: 12;
margin-start: 6; margin-start: 6;
margin-end: 6; margin-end: 6;
} }
Label description { Label description {
styles ["title-4"] styles ["title-4"]
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
justify: center; justify: center;
margin-start: 6; margin-start: 6;
margin-end: 6; margin-end: 6;
} }
Box { Box {
spacing: 6; spacing: 6;
homogeneous: true; homogeneous: true;
margin-top: 18; margin-top: 18;
margin-bottom: 12; margin-bottom: 12;
halign: center; halign: center;
Button open_app_button { Button open_app_button {
styles ["suggested-action", "pill"] styles ["suggested-action", "pill"]
can-shrink: true; can-shrink: true;
label: _("Open"); label: _("Open");
} }
Button uninstall_button { Button uninstall_button {
styles ["pill"] styles ["pill"]
can-shrink: true; can-shrink: true;
label: _("Uninstall"); label: _("Uninstall");
} }
} }
Box eol_box { Box eol_box {
margin-bottom: 12; margin-bottom: 12;
styles ["card"] styles ["card"]
Label status_label { Label status_label {
margin-top: 6; margin-top: 6;
margin-bottom: 7; margin-bottom: 7;
margin-start: 6; margin-start: 6;
margin-end: 6; margin-end: 6;
label: _("This package is End Of Life, and will not receive any security updates"); label: _("This package is End Of Life, and will not receive any security updates");
styles ["heading", "error"] styles ["heading", "error"]
halign: center; halign: center;
hexpand: true; hexpand: true;
wrap: true; wrap: true;
justify: center; justify: center;
} }
} }
Box information { Box information {
orientation: vertical; orientation: vertical;
Adw.PreferencesGroup actions { Adw.PreferencesGroup actions {
margin-bottom: 12; margin-bottom: 12;
Adw.ActionRow data_row { Adw.ActionRow data_row {
title: _("User Data"); title: _("User Data");
styles["property"] styles["property"]
[suffix] [suffix]
Button open_data_button { Button open_data_button {
styles["flat"] styles["flat"]
valign: center; valign: center;
icon-name: "folder-open-symbolic"; icon-name: "folder-open-symbolic";
tooltip-text: _("Open User Data"); tooltip-text: _("Open User Data");
} }
[suffix] [suffix]
Button trash_data_button { Button trash_data_button {
styles["flat"] styles["flat"]
valign: center; valign: center;
icon-name: "user-trash-symbolic"; icon-name: "user-trash-symbolic";
tooltip-text: _("Trash User Data"); tooltip-text: _("Trash User Data");
} }
[suffix] [suffix]
Spinner data_spinner { Spinner data_spinner {
spinning: true; spinning: true;
} }
} }
Adw.ExpanderRow version_row { Adw.ExpanderRow version_row {
title: _("Version"); title: _("Version");
styles ["property"] styles ["property"]
[suffix] [suffix]
Label mask_label { Label mask_label {
label: _("Updates Disabled"); label: _("Updates Disabled");
styles["warning"] styles["warning"]
} }
Adw.ActionRow mask_row { Adw.ActionRow mask_row {
title: _("Disable Updates"); title: _("Disable Updates");
subtitle: _("Mask this package so it's never updated"); subtitle: _("Mask this package so it's never updated");
activatable: true; activatable: true;
Gtk.Switch mask_switch { Gtk.Switch mask_switch {
valign: center; valign: center;
can-focus: false; can-focus: false;
can-target: false; can-target: false;
} }
} }
Adw.ActionRow change_version_row { Adw.ActionRow change_version_row {
title: _("Change Version"); title: _("Change Version");
subtitle: _("Upgrade or downgrade this package"); subtitle: _("Upgrade or downgrade this package");
activatable: true; activatable: true;
Image { Image {
icon-name: "right-large-symbolic"; icon-name: "right-large-symbolic";
} }
} }
} }
Adw.ActionRow installed_size_row { Adw.ActionRow installed_size_row {
styles ["property"] styles ["property"]
title: _("Installed Size"); title: _("Installed Size");
activatable: true; activatable: true;
Image { Image {
icon-name: "copy-symbolic"; icon-name: "copy-symbolic";
} }
} }
Adw.ActionRow runtime_row { Adw.ActionRow runtime_row {
styles ["property"] styles ["property"]
title: _("Runtime"); title: _("Runtime");
activatable: true; activatable: true;
Image eol_package_package_status_icon { Image eol_package_package_status_icon {
icon-name: "error-symbolic"; icon-name: "error-symbolic";
tooltip-text: _("This package is End Of Life, and will not recieve any security updates"); tooltip-text: _("This package is End Of Life, and will not recieve any security updates");
margin-end: 6; margin-end: 6;
styles["error"] styles["error"]
} }
Image { Image {
icon-name: "right-large-symbolic"; icon-name: "right-large-symbolic";
} }
} }
Adw.ActionRow pin_row { Adw.ActionRow pin_row {
title: _("Disable Automactic Removal"); title: _("Disable Automactic Removal");
subtitle: _("Pin this runtime to keep it installed"); subtitle: _("Pin this runtime to keep it installed");
activatable: true; activatable: true;
Gtk.Switch pin_switch { Gtk.Switch pin_switch {
valign: center; valign: center;
can-focus: false; can-focus: false;
can-target: false; can-target: false;
} }
} }
} }
Adw.PreferencesGroup package_info { Adw.PreferencesGroup package_info {
margin-bottom: 12; margin-bottom: 12;
title: _("Package Information"); title: _("Package Information");
Adw.ActionRow id_row { Adw.ActionRow id_row {
styles ["property"] styles ["property"]
title: _("Application ID"); title: _("Application ID");
activatable: true; activatable: true;
Image { Image {
icon-name: "copy-symbolic"; icon-name: "copy-symbolic";
} }
} }
Adw.ActionRow ref_row { Adw.ActionRow ref_row {
styles ["property"] styles ["property"]
title: "Ref"; title: "Ref";
activatable: true; activatable: true;
Image { Image {
icon-name: "copy-symbolic"; icon-name: "copy-symbolic";
} }
} }
Adw.ActionRow arch_row { Adw.ActionRow arch_row {
styles ["property"] styles ["property"]
title: _("Architecture"); title: _("Architecture");
activatable: true; activatable: true;
Image { Image {
icon-name: "copy-symbolic"; icon-name: "copy-symbolic";
} }
} }
Adw.ActionRow branch_row { Adw.ActionRow branch_row {
styles ["property"] styles ["property"]
title: _("Branch"); title: _("Branch");
activatable: true; activatable: true;
Image { Image {
icon-name: "copy-symbolic"; icon-name: "copy-symbolic";
} }
} }
Adw.ActionRow license_row { Adw.ActionRow license_row {
styles ["property"] styles ["property"]
title: _("License"); title: _("License");
activatable: true; activatable: true;
Image { Image {
icon-name: "copy-symbolic"; icon-name: "copy-symbolic";
} }
} }
} }
Adw.PreferencesGroup remote_info { Adw.PreferencesGroup remote_info {
margin-bottom: 12; margin-bottom: 12;
title: _("Installation Information"); title: _("Installation Information");
Adw.ActionRow sdk_row { Adw.ActionRow sdk_row {
styles ["property"] styles ["property"]

View File

@@ -9,382 +9,382 @@ import subprocess, os
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/properties_page/properties_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/properties_page/properties_page.ui")
class PropertiesPage(Adw.NavigationPage): class PropertiesPage(Adw.NavigationPage):
__gtype_name__ = 'PropertiesPage' __gtype_name__ = 'PropertiesPage'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
stack = gtc() stack = gtc()
error_tbv = gtc() error_tbv = gtc()
loading_tbv = gtc() loading_tbv = gtc()
more_menu = gtc() more_menu = gtc()
more_list = gtc() more_list = gtc()
nav_view = gtc() nav_view = gtc()
inner_nav_page = gtc() inner_nav_page = gtc()
toast_overlay = gtc() toast_overlay = gtc()
header_bar = gtc() header_bar = gtc()
scrolled_window = gtc() scrolled_window = gtc()
app_icon = gtc() app_icon = gtc()
name = gtc() name = gtc()
description = gtc() description = gtc()
eol_box = gtc() eol_box = gtc()
open_app_button = gtc() open_app_button = gtc()
uninstall_button = gtc() uninstall_button = gtc()
pin_row = gtc() pin_row = gtc()
pin_switch = gtc() pin_switch = gtc()
data_row = gtc() data_row = gtc()
open_data_button = gtc() open_data_button = gtc()
trash_data_button = gtc() trash_data_button = gtc()
data_spinner = gtc() data_spinner = gtc()
version_row = gtc() version_row = gtc()
mask_label = gtc() mask_label = gtc()
mask_row = gtc() mask_row = gtc()
mask_switch = gtc() mask_switch = gtc()
change_version_row = gtc() change_version_row = gtc()
installed_size_row = gtc() installed_size_row = gtc()
runtime_row = gtc() runtime_row = gtc()
eol_package_package_status_icon = gtc() eol_package_package_status_icon = gtc()
id_row = gtc() id_row = gtc()
ref_row = gtc() ref_row = gtc()
arch_row = gtc() arch_row = gtc()
branch_row = gtc() branch_row = gtc()
license_row = gtc() license_row = gtc()
sdk_row = gtc() sdk_row = gtc()
origin_row = gtc() origin_row = gtc()
collection_row = gtc() collection_row = gtc()
installation_row = gtc() installation_row = gtc()
commit_row = gtc() commit_row = gtc()
parent_row = gtc() parent_row = gtc()
subject_row = gtc() subject_row = gtc()
date_row = gtc() date_row = gtc()
package = None package = None
def set_properties(self, package, refresh=False): def set_properties(self, package, refresh=False):
if package == self.package and not refresh: if package == self.package and not refresh:
# Do not update the ui if the same app row is clicked # Do not update the ui if the same app row is clicked
return return
self.reinstall_did_error = False self.reinstall_did_error = False
self.package = package self.package = package
pkg_name = package.info["name"] pkg_name = package.info["name"]
if pkg_name != "": if pkg_name != "":
self.inner_nav_page.set_title(_("{} Properties").format(package.info["name"])) self.inner_nav_page.set_title(_("{} Properties").format(package.info["name"]))
self.name.set_visible(True) self.name.set_visible(True)
self.name.set_label(pkg_name) self.name.set_label(pkg_name)
else: else:
self.name.set_visible(False) self.name.set_visible(False)
self.inner_nav_page.set_title(_("Properties")) self.inner_nav_page.set_title(_("Properties"))
if package.icon_path: if package.icon_path:
GLib.idle_add(lambda *_: self.app_icon.set_from_file(package.icon_path)) GLib.idle_add(lambda *_: self.app_icon.set_from_file(package.icon_path))
else: else:
GLib.idle_add(lambda *_: self.app_icon.set_from_icon_name("application-x-executable-symbolic")) GLib.idle_add(lambda *_: self.app_icon.set_from_icon_name("application-x-executable-symbolic"))
self.eol_box.set_visible(package.is_eol) self.eol_box.set_visible(package.is_eol)
self.pin_row.set_visible(package.is_runtime) self.pin_row.set_visible(package.is_runtime)
self.open_app_button.set_visible(package.is_runtime) self.open_app_button.set_visible(package.is_runtime)
self.open_app_button.set_visible(not package.is_runtime) self.open_app_button.set_visible(not package.is_runtime)
self.data_row.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") self.uninstall_button.set_sensitive(self.package.info['id'] != "io.github.flattool.Warehouse")
if package.is_runtime: if package.is_runtime:
self.runtime_row.set_visible(False) self.runtime_row.set_visible(False)
else: else:
has_path = os.path.exists(package.data_path) has_path = os.path.exists(package.data_path)
self.trash_data_button.set_sensitive(has_path and self.package.info['id'] != "io.github.flattool.Warehouse") self.trash_data_button.set_sensitive(has_path and self.package.info['id'] != "io.github.flattool.Warehouse")
self.open_data_button.set_sensitive(has_path) self.open_data_button.set_sensitive(has_path)
if not self.package.dependent_runtime is None: if not self.package.dependent_runtime is None:
self.runtime_row.set_visible(True) self.runtime_row.set_visible(True)
self.runtime_row.set_subtitle(self.package.dependent_runtime.info["name"]) self.runtime_row.set_subtitle(self.package.dependent_runtime.info["name"])
self.eol_package_package_status_icon.set_visible(self.package.dependent_runtime.is_eol) self.eol_package_package_status_icon.set_visible(self.package.dependent_runtime.is_eol)
if has_path: if has_path:
self.trash_data_button.set_visible(False) self.trash_data_button.set_visible(False)
self.open_data_button.set_visible(False) self.open_data_button.set_visible(False)
self.data_spinner.set_visible(True) self.data_spinner.set_visible(True)
self.data_row.set_subtitle(_("Loading User Data")) self.data_row.set_subtitle(_("Loading User Data"))
def callback(size): def callback(size):
self.trash_data_button.set_visible(True) self.trash_data_button.set_visible(True)
self.open_data_button.set_visible(True) self.open_data_button.set_visible(True)
self.data_spinner.set_visible(False) self.data_spinner.set_visible(False)
self.data_row.set_subtitle(size) self.data_row.set_subtitle(size)
self.package.get_data_size(lambda size: callback(size)) self.package.get_data_size(lambda size: callback(size))
else: else:
self.data_row.set_subtitle(_("No User Data")) self.data_row.set_subtitle(_("No User Data"))
self.data_spinner.set_visible(False) self.data_spinner.set_visible(False)
cli_info = None cli_info = None
try: try:
cli_info = package.get_cli_info() cli_info = package.get_cli_info()
pkg_description = package.cli_info["description"] pkg_description = package.cli_info["description"]
self.description.set_visible(pkg_description != "") self.description.set_visible(pkg_description != "")
self.description.set_label(pkg_description) self.description.set_label(pkg_description)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast)
return return
for key, row in self.info_rows.items(): for key, row in self.info_rows.items():
row.set_visible(False) row.set_visible(False)
try: try:
subtitle = cli_info[key] subtitle = cli_info[key]
row.set_subtitle(subtitle) row.set_subtitle(subtitle)
row.set_visible(True) row.set_visible(True)
except KeyError: except KeyError:
if key == "version": if key == "version":
row.set_visible(True) row.set_visible(True)
row.set_subtitle(_("No version information found")) row.set_subtitle(_("No version information found"))
continue continue
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast)
continue continue
self.mask_label.set_visible(package.is_masked) self.mask_label.set_visible(package.is_masked)
self.mask_switch.set_active(package.is_masked) self.mask_switch.set_active(package.is_masked)
self.pin_switch.set_active(package.is_pinned) self.pin_switch.set_active(package.is_pinned)
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.nav_view)) GLib.idle_add(lambda *_: self.stack.set_visible_child(self.nav_view))
self.more_list.remove_all() self.more_list.remove_all()
if self.open_app_button.get_visible(): if self.open_app_button.get_visible():
self.more_list.append(self.view_snapshots) self.more_list.append(self.view_snapshots)
self.more_list.append(self.copy_launch_command) self.more_list.append(self.copy_launch_command)
self.more_list.append(self.show_details) self.more_list.append(self.show_details)
self.more_list.append(self.reinstall) self.more_list.append(self.reinstall)
def open_data_handler(self, *args): def open_data_handler(self, *args):
if error := self.package.open_data(): if error := self.package.open_data():
self.toast_overlay.add_toast(ErrorToast(_("Could not open data"), str(error)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open data"), str(error)).toast)
def trash_data_handler(self, *args): def trash_data_handler(self, *args):
def on_choice(dialog, response): def on_choice(dialog, response):
if response != 'continue': if response != 'continue':
return return
try: try:
self.package.trash_data() self.package.trash_data()
self.set_properties(self.package, refresh=True) self.set_properties(self.package, refresh=True)
self.toast_overlay.add_toast(Adw.Toast.new("Trashed User Data")) 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 = HostInfo.main_window.pages[HostInfo.main_window.user_data_row]
user_data_page.start_loading() user_data_page.start_loading()
user_data_page.end_loading() user_data_page.end_loading()
snapshot_list_page = HostInfo.main_window.pages[HostInfo.main_window.snapshots_row].list_page snapshot_list_page = HostInfo.main_window.pages[HostInfo.main_window.snapshots_row].list_page
snapshot_list_package = snapshot_list_page.package_or_folder snapshot_list_package = snapshot_list_page.package_or_folder
if not snapshot_list_package is None: if not snapshot_list_package is None:
snapshot_list_page.set_snapshots(snapshot_list_package, True) snapshot_list_page.set_snapshots(snapshot_list_package, True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast)
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Send {}'s User Data to the Trash?").format(self.package.info["name"]), heading=_("Send {}'s User Data to the Trash?").format(self.package.info["name"]),
body=_("Your settings and data for this app will be sent to the trash") body=_("Your settings and data for this app will be sent to the trash")
) )
dialog.add_response('cancel', _("Cancel")) dialog.add_response('cancel', _("Cancel"))
dialog.add_response('continue', _("Trash Data")) dialog.add_response('continue', _("Trash Data"))
dialog.connect("response", on_choice) dialog.connect("response", on_choice)
dialog.set_response_appearance('continue', Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance('continue', Adw.ResponseAppearance.DESTRUCTIVE)
dialog.present(self.main_window) dialog.present(self.main_window)
def set_mask_handler(self, *args): def set_mask_handler(self, *args):
state = not self.mask_switch.get_active() state = not self.mask_switch.get_active()
def callback(*args): def callback(*args):
if fail := self.package.failed_mask: if fail := self.package.failed_mask:
response = _("Could not Disable Updates") if state else _("Could not Enable Updates") response = _("Could not Disable Updates") if state else _("Could not Enable Updates")
fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail
self.toast_overlay.add_toast(ErrorToast(response, str(fail)).toast) 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_switch.set_active(not state))
GLib.idle_add(lambda *_: self.mask_label.set_visible(not state)) GLib.idle_add(lambda *_: self.mask_label.set_visible(not state))
else: else:
response = _("Disabled Updates") if state else _("Enabled Updates") response = _("Disabled Updates") if state else _("Enabled Updates")
self.toast_overlay.add_toast(Adw.Toast(title=response)) 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_switch.set_active(state))
GLib.idle_add(lambda *_: self.mask_label.set_visible(state)) GLib.idle_add(lambda *_: self.mask_label.set_visible(state))
self.package.app_row.masked_status_icon.set_visible(state) self.package.app_row.masked_status_icon.set_visible(state)
self.package.set_mask(state, callback) self.package.set_mask(state, callback)
def set_pin_handler(self, *args): def set_pin_handler(self, *args):
state = not self.pin_switch.get_active() state = not self.pin_switch.get_active()
def callback(*args): def callback(*args):
if fail := self.package.failed_pin: if fail := self.package.failed_pin:
response = _("Could not Disable Autoremoval") if state else _("Could not Enable Autoremoval") response = _("Could not Disable Autoremoval") if state else _("Could not Enable Autoremoval")
fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail
self.toast_overlay.add_toast(ErrorToast(response, str(fail)).toast) self.toast_overlay.add_toast(ErrorToast(response, str(fail)).toast)
GLib.idle_add(lambda *_: self.pin_switch.set_active(not state)) GLib.idle_add(lambda *_: self.pin_switch.set_active(not state))
else: else:
response = _("Disabled Autoremoval") if state else _("Enabled Autoremoval") response = _("Disabled Autoremoval") if state else _("Enabled Autoremoval")
self.toast_overlay.add_toast(Adw.Toast(title=response)) self.toast_overlay.add_toast(Adw.Toast(title=response))
GLib.idle_add(lambda *_: self.pin_switch.set_active(state)) GLib.idle_add(lambda *_: self.pin_switch.set_active(state))
self.package.app_row.pinned_status_icon.set_visible(state) self.package.app_row.pinned_status_icon.set_visible(state)
self.package.set_pin(state, callback) self.package.set_pin(state, callback)
def uninstall_handler(self, *args): def uninstall_handler(self, *args):
def on_choice(should_trash): def on_choice(should_trash):
self.packages_page.set_status(self.packages_page.uninstalling) self.packages_page.set_status(self.packages_page.uninstalling)
self.package.uninstall(callback) self.package.uninstall(callback)
if should_trash: if should_trash:
try: try:
self.package.trash_data() self.package.trash_data()
self.set_properties(self.package, refresh=True) self.set_properties(self.package, refresh=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast)
def callback(*args): def callback(*args):
if fail := self.package.failed_uninstall: if fail := self.package.failed_uninstall:
fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail
self.toast_overlay.add_toast(ErrorToast(_("Could not uninstall"), str(fail)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not uninstall"), str(fail)).toast)
self.packages_page.set_status(self.packages_page.scrolled_window) self.packages_page.set_status(self.packages_page.scrolled_window)
else: else:
self.main_window.refresh_handler() self.main_window.refresh_handler()
HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled {}").format(self.package.info["name"]))) HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled {}").format(self.package.info["name"])))
dialog = UninstallDialog(on_choice, os.path.exists(self.package.data_path), self.package.info["name"]) dialog = UninstallDialog(on_choice, os.path.exists(self.package.data_path), self.package.info["name"])
dialog.present(self.main_window) dialog.present(self.main_window)
def runtime_row_handler(self, *args): def runtime_row_handler(self, *args):
new_page = self.__class__() new_page = self.__class__()
new_page.packages_page = self.packages_page new_page.packages_page = self.packages_page
new_page.set_properties(self.package.dependent_runtime) new_page.set_properties(self.package.dependent_runtime)
self.nav_view.push(new_page) self.nav_view.push(new_page)
def open_app_handler(self, *args): def open_app_handler(self, *args):
self.toast_overlay.add_toast(Adw.Toast(title=_("Opening {}").format(self.package.info["name"]))) self.toast_overlay.add_toast(Adw.Toast(title=_("Opening {}").format(self.package.info["name"])))
def callback(*args): def callback(*args):
if fail := self.package.failed_app_run: if fail := self.package.failed_app_run:
fail = fail.stderr if type(fail) == subprocess.CalledProcessError else fail 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.toast_overlay.add_toast(ErrorToast(_("Could not open {}").format(self.package.info["name"]), str(fail)).toast)
self.package.open_app(callback) self.package.open_app(callback)
def copy_handler(self, row): def copy_handler(self, row):
HostInfo.clipboard.set(row.get_subtitle()) HostInfo.clipboard.set(row.get_subtitle())
self.toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(row.get_title()))) self.toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(row.get_title())))
def change_version_handler(self, row): def change_version_handler(self, row):
page = ChangeVersionPage(self.packages_page, self.package) page = ChangeVersionPage(self.packages_page, self.package)
self.nav_view.push(page) self.nav_view.push(page)
def reinstall_callback(self): def reinstall_callback(self):
HostInfo.main_window.refresh_handler() HostInfo.main_window.refresh_handler()
if not self.reinstall_did_error: if not self.reinstall_did_error:
HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Reinstalled {}").format(self.package.info['name']))) HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Reinstalled {}").format(self.package.info['name'])))
def reinstall_error_callback(self, user_facing_label, error_message): def reinstall_error_callback(self, user_facing_label, error_message):
self.reinstall_did_error = True self.reinstall_did_error = True
GLib.idle_add(lambda *_: HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast)) GLib.idle_add(lambda *_: HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast))
def reinstall_handler(self): def reinstall_handler(self):
def on_response(dialog, response): def on_response(dialog, response):
if response != "continue": if response != "continue":
return return
self.reinstall_did_error = False self.reinstall_did_error = False
PackageInstallWorker.install( PackageInstallWorker.install(
[{ [{
"installation": self.package.info['installation'], "installation": self.package.info['installation'],
"remote": self.package.info['origin'], "remote": self.package.info['origin'],
"package_names": [self.package.info['ref']], "package_names": [self.package.info['ref']],
"extra_flags": ["--reinstall"], "extra_flags": ["--reinstall"],
}], }],
self.packages_page.reinstalling, self.packages_page.reinstalling,
self.reinstall_callback, self.reinstall_callback,
self.reinstall_error_callback, self.reinstall_error_callback,
) )
self.packages_page.set_status(self.packages_page.reinstalling) self.packages_page.set_status(self.packages_page.reinstalling)
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Reinstall {}?").format(self.package.info['name']), heading=_("Reinstall {}?").format(self.package.info['name']),
body=_("This package will be uninstalled, and then reinstalled from the same remote and installation.") body=_("This package will be uninstalled, and then reinstalled from the same remote and installation.")
) )
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Reinstall")) dialog.add_response("continue", _("Reinstall"))
dialog.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED) dialog.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def more_menu_handler(self, listbox, row): def more_menu_handler(self, listbox, row):
self.more_menu.popdown() self.more_menu.popdown()
match row.get_child(): match row.get_child():
case self.view_snapshots: case self.view_snapshots:
snapshots_row = HostInfo.main_window.snapshots_row snapshots_row = HostInfo.main_window.snapshots_row
snapshots_page = HostInfo.main_window.pages[snapshots_row] snapshots_page = HostInfo.main_window.pages[snapshots_row]
HostInfo.main_window.activate_row(snapshots_row) HostInfo.main_window.activate_row(snapshots_row)
snapshots_page.show_snapshot(self.package) snapshots_page.show_snapshot(self.package)
case self.copy_launch_command: case self.copy_launch_command:
try: try:
HostInfo.clipboard.set(f"flatpak run {self.package.info['ref']}") HostInfo.clipboard.set(f"flatpak run {self.package.info['ref']}")
self.toast_overlay.add_toast(Adw.Toast.new(_("Copied launch command"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Copied launch command")))
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not copy launch command"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not copy launch command"), str(e)).toast)
case self.show_details: case self.show_details:
try: try:
Gio.AppInfo.launch_default_for_uri(f"appstream://{self.package.info['id']}", None) Gio.AppInfo.launch_default_for_uri(f"appstream://{self.package.info['id']}", None)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not show details"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not show details"), str(e)).toast)
case self.reinstall: case self.reinstall:
self.reinstall_handler() self.reinstall_handler()
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.main_window = HostInfo.main_window self.main_window = HostInfo.main_window
self.info_rows = { self.info_rows = {
"version": self.version_row, "version": self.version_row,
"installed": self.installed_size_row, "installed": self.installed_size_row,
"id": self.id_row, "id": self.id_row,
"ref": self.ref_row, "ref": self.ref_row,
"arch": self.arch_row, "arch": self.arch_row,
"branch": self.branch_row, "branch": self.branch_row,
"license": self.license_row, "license": self.license_row,
"sdk": self.sdk_row, "sdk": self.sdk_row,
"origin": self.origin_row, "origin": self.origin_row,
"collection": self.collection_row, "collection": self.collection_row,
"installation": self.installation_row, "installation": self.installation_row,
"commit": self.commit_row, "commit": self.commit_row,
"parent": self.parent_row, "parent": self.parent_row,
"subject": self.subject_row, "subject": self.subject_row,
"date": self.date_row, "date": self.date_row,
} }
self.loading_tbv.set_content(LoadingStatus(_("Loading Properties"), _("This should only take a moment"))) 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.packages_page = None # To be set in packages page
self.__class__.main_window = self.main_window self.__class__.main_window = self.main_window
self.view_snapshots = Gtk.Label(halign=Gtk.Align.START, label=_("View Snapshots")) 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.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.show_details = Gtk.Label(halign=Gtk.Align.START, label=_("Show Details"))
self.reinstall = Gtk.Label(halign=Gtk.Align.START, label=_("Reinstall")) self.reinstall = Gtk.Label(halign=Gtk.Align.START, label=_("Reinstall"))
self.reinstall_did_error = False self.reinstall_did_error = False
# Apply # Apply
# Connections # Connections
self.more_list.connect("row-activated", self.more_menu_handler) self.more_list.connect("row-activated", self.more_menu_handler)
self.open_data_button.connect("clicked", self.open_data_handler) self.open_data_button.connect("clicked", self.open_data_handler)
self.scrolled_window.get_vadjustment().connect("value-changed", lambda adjustment: self.header_bar.set_show_title(not adjustment.get_value() == 0)) 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.trash_data_button.connect("clicked", self.trash_data_handler)
self.runtime_row.connect("activated", self.runtime_row_handler) self.runtime_row.connect("activated", self.runtime_row_handler)
self.open_app_button.connect("clicked", self.open_app_handler) self.open_app_button.connect("clicked", self.open_app_handler)
self.uninstall_button.connect("clicked", self.uninstall_handler) self.uninstall_button.connect("clicked", self.uninstall_handler)
self.mask_row.connect("activated", self.set_mask_handler) self.mask_row.connect("activated", self.set_mask_handler)
self.pin_row.connect("activated", self.set_pin_handler) self.pin_row.connect("activated", self.set_pin_handler)
self.change_version_row.connect("activated", self.change_version_handler) self.change_version_row.connect("activated", self.change_version_handler)
for key, row in self.info_rows.items(): for key, row in self.info_rows.items():
if type(row) is Adw.ActionRow: if type(row) is Adw.ActionRow:
row.connect("activated", self.copy_handler) row.connect("activated", self.copy_handler)

View File

@@ -2,55 +2,55 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $AddRemoteDialog : Adw.Dialog { template $AddRemoteDialog : Adw.Dialog {
title: _("Add a Remote"); title: _("Add a Remote");
// content-width: 500; // content-width: 500;
// content-height: 375; // content-height: 375;
// width-request: 400; // width-request: 400;
follows-content-size: true; follows-content-size: true;
Adw.ToolbarView { Adw.ToolbarView {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
show-start-title-buttons: false; show-start-title-buttons: false;
show-end-title-buttons: false; show-end-title-buttons: false;
[start] [start]
Button cancel_button { Button cancel_button {
label: _("Cancel"); label: _("Cancel");
} }
[end] [end]
Button apply_button { Button apply_button {
styles ["suggested-action"] styles ["suggested-action"]
label: _("Add"); label: _("Add");
} }
} }
Adw.ToastOverlay toast_overlay { Adw.ToastOverlay toast_overlay {
ScrolledWindow content_page { ScrolledWindow content_page {
propagate-natural-height: true; propagate-natural-height: true;
propagate-natural-width: true; propagate-natural-width: true;
Adw.Clamp { Adw.Clamp {
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-top: 12; margin-top: 12;
margin-bottom: 12; margin-bottom: 12;
Box { Box {
orientation: vertical; orientation: vertical;
spacing: 12; spacing: 12;
Adw.PreferencesGroup { Adw.PreferencesGroup {
styles ["boxed-list"] styles ["boxed-list"]
Adw.EntryRow title_row { Adw.EntryRow title_row {
title: _("Title"); title: _("Title");
} }
Adw.EntryRow name_row { Adw.EntryRow name_row {
title: _("Name"); title: _("Name");
} }
Adw.EntryRow url_row { Adw.EntryRow url_row {
title: _("Repo URL"); title: _("Repo URL");
} }
} }
$InstallationChooser installation_chooser { $InstallationChooser installation_chooser {
} }
} }
} }
} }
} }
} }
} }

View File

@@ -7,125 +7,125 @@ import subprocess, re
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/add_remote_dialog.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/add_remote_dialog.ui")
class AddRemoteDialog(Adw.Dialog): class AddRemoteDialog(Adw.Dialog):
__gtype_name__ = "AddRemoteDialog" __gtype_name__ = "AddRemoteDialog"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
toast_overlay = gtc() toast_overlay = gtc()
cancel_button = gtc() cancel_button = gtc()
apply_button = gtc() apply_button = gtc()
content_page = gtc() content_page = gtc()
title_row = gtc() title_row = gtc()
name_row = gtc() name_row = gtc()
url_row = gtc() url_row = gtc()
installation_chooser = gtc() installation_chooser = gtc()
is_open = False is_open = False
def on_apply(self, *args): def on_apply(self, *args):
self.parent_page.status_stack.set_visible_child(self.parent_page.adding_view) self.parent_page.status_stack.set_visible_child(self.parent_page.adding_view)
self.apply_button.set_sensitive(False) self.apply_button.set_sensitive(False)
error = [None] error = [None]
def thread(*args): def thread(*args):
HostInfo.main_window.add_refresh_lockout("adding remote") HostInfo.main_window.add_refresh_lockout("adding remote")
cmd = [ cmd = [
'flatpak-spawn', '--host', 'flatpak-spawn', '--host',
'flatpak', 'remote-add', 'flatpak', 'remote-add',
f'--title={self.title_row.get_text()}', f'--title={self.title_row.get_text()}',
self.name_row.get_text(), self.name_row.get_text(),
self.url_row.get_text(), self.url_row.get_text(),
] ]
installation = self.installation_chooser.get_installation() installation = self.installation_chooser.get_installation()
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error[0] = cpe.stderr error[0] = cpe.stderr
except Exception as e: except Exception as e:
error[0] = e error[0] = e
def callback(*args): def callback(*args):
HostInfo.main_window.remove_refresh_lockout("adding remote") HostInfo.main_window.remove_refresh_lockout("adding remote")
if error[0]: if error[0]:
self.parent_page.status_stack.set_visible_child(self.parent_page.main_view) self.parent_page.status_stack.set_visible_child(self.parent_page.main_view)
self.apply_button.set_sensitive(True) self.apply_button.set_sensitive(True)
self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not add remote"), str(error[0])).toast) self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not add remote"), str(error[0])).toast)
else: else:
self.main_window.refresh_handler() self.main_window.refresh_handler()
self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Added {}").format(self.title_row.get_text()))) self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Added {}").format(self.title_row.get_text())))
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
self.close() self.close()
def check_entries(self, row): def check_entries(self, row):
is_passing = re.match(self.rexes[row], row.get_text()) is_passing = re.match(self.rexes[row], row.get_text())
if is_passing: if is_passing:
row.remove_css_class("error") row.remove_css_class("error")
else: else:
row.add_css_class("error") row.add_css_class("error")
match row: match row:
case self.title_row: case self.title_row:
self.title_passes = bool(is_passing) self.title_passes = bool(is_passing)
case self.name_row: case self.name_row:
self.name_passes = bool(is_passing) self.name_passes = bool(is_passing)
case self.url_row: case self.url_row:
self.url_passes = bool(is_passing) self.url_passes = bool(is_passing)
self.apply_button.set_sensitive(self.title_passes and self.name_passes and self.url_passes) self.apply_button.set_sensitive(self.title_passes and self.name_passes and self.url_passes)
def present(self, *args, **kwargs): def present(self, *args, **kwargs):
if self.__class__.is_open: if self.__class__.is_open:
return return
self.__class__.is_open = True self.__class__.is_open = True
super().present(*args, **kwargs) super().present(*args, **kwargs)
def on_close(self, *args): def on_close(self, *args):
self.__class__.is_open = False self.__class__.is_open = False
def __init__(self, main_window, parent_page, remote_info=None, **kwargs): def __init__(self, main_window, parent_page, remote_info=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.string_list = Gtk.StringList(strings=HostInfo.installations) self.string_list = Gtk.StringList(strings=HostInfo.installations)
self.main_window = main_window self.main_window = main_window
self.parent_page = parent_page self.parent_page = parent_page
self.rexes = { self.rexes = {
self.title_row: r"^(?=.*[A-Za-z0-9])[A-Za-z0-9._-]+( +[A-Za-z0-9._-]+)*$", self.title_row: r"^(?=.*[A-Za-z0-9])[A-Za-z0-9._-]+( +[A-Za-z0-9._-]+)*$",
self.name_row: r"^[a-zA-Z0-9\-._]+$", self.name_row: r"^[a-zA-Z0-9\-._]+$",
self.url_row: r"^[a-zA-Z0-9\-._~:/?#[\]@!$&\'()*+,;= ]+$", self.url_row: r"^[a-zA-Z0-9\-._~:/?#[\]@!$&\'()*+,;= ]+$",
} }
self.title_passes = False self.title_passes = False
self.name_passes = False self.name_passes = False
self.url_passes = False self.url_passes = False
# Apply # Apply
self.installation_chooser.set_content_strings(_("remote"), False) self.installation_chooser.set_content_strings(_("remote"), False)
if remote_info: if remote_info:
self.title_row.set_text(remote_info["title"]) self.title_row.set_text(remote_info["title"])
self.name_row.set_text(remote_info["name"]) self.name_row.set_text(remote_info["name"])
self.url_row.set_text(remote_info["link"]) self.url_row.set_text(remote_info["link"])
if remote_info["description"] == "local file": if remote_info["description"] == "local file":
self.check_entries(self.title_row) self.check_entries(self.title_row)
self.check_entries(self.name_row) self.check_entries(self.name_row)
self.check_entries(self.url_row) self.check_entries(self.url_row)
self.url_row.set_editable(False) self.url_row.set_editable(False)
else: else:
self.title_row.set_editable(False) self.title_row.set_editable(False)
self.name_row.set_editable(False) self.name_row.set_editable(False)
self.url_row.set_editable(False) self.url_row.set_editable(False)
self.apply_button.set_sensitive(True) self.apply_button.set_sensitive(True)
else: else:
self.apply_button.set_sensitive(False) self.apply_button.set_sensitive(False)
# Connections # Connections
self.connect("closed", self.on_close) self.connect("closed", self.on_close)
self.cancel_button.connect("clicked", lambda *_: self.close()) self.cancel_button.connect("clicked", lambda *_: self.close())
self.apply_button.connect("clicked", self.on_apply) self.apply_button.connect("clicked", self.on_apply)
self.title_row.connect("changed", self.check_entries) self.title_row.connect("changed", self.check_entries)
self.name_row.connect("changed", self.check_entries) self.name_row.connect("changed", self.check_entries)
self.url_row.connect("changed", self.check_entries) self.url_row.connect("changed", self.check_entries)

View File

@@ -2,56 +2,56 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $RemoteRow : Adw.ActionRow { template $RemoteRow : Adw.ActionRow {
[suffix] [suffix]
Label suffix_label { Label suffix_label {
styles ["subtitle"] styles ["subtitle"]
margin-end: 6; margin-end: 6;
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
natural-wrap-mode: none; natural-wrap-mode: none;
halign: end; halign: end;
hexpand: true; hexpand: true;
justify: right; justify: right;
} }
[suffix] [suffix]
Button filter_button { Button filter_button {
styles ["flat"] styles ["flat"]
valign: center; valign: center;
icon-name: "funnel-symbolic"; icon-name: "funnel-symbolic";
tooltip-text: _("Set a Filter for this Remote"); tooltip-text: _("Set a Filter for this Remote");
} }
[suffix] [suffix]
MenuButton menu_button { MenuButton menu_button {
styles ["flat"] styles ["flat"]
valign: center; valign: center;
popover: menu_pop; popover: menu_pop;
icon-name: "view-more-symbolic"; icon-name: "view-more-symbolic";
tooltip-text: _("More Actions"); tooltip-text: _("More Actions");
} }
} }
Popover menu_pop { Popover menu_pop {
styles ["menu"] styles ["menu"]
ListBox menu_listbox { ListBox menu_listbox {
Label copy_title { Label copy_title {
label: _("Copy Title"); label: _("Copy Title");
halign: start; halign: start;
} }
Label copy_name { Label copy_name {
label: _("Copy Name"); label: _("Copy Name");
halign: start; halign: start;
} }
Label enable_remote { Label enable_remote {
label: _("Enable"); label: _("Enable");
halign: start; halign: start;
} }
Label disable_remote { Label disable_remote {
label: _("Disable"); label: _("Disable");
halign: start; halign: start;
} }
Label remove { Label remove {
label: _("Remove"); label: _("Remove");
halign: start; halign: start;
} }
} }
} }

View File

@@ -5,168 +5,168 @@ import subprocess
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remote_row.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remote_row.ui")
class RemoteRow(Adw.ActionRow): class RemoteRow(Adw.ActionRow):
__gtype_name__ = 'RemoteRow' __gtype_name__ = 'RemoteRow'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
suffix_label = gtc() suffix_label = gtc()
filter_button = gtc() filter_button = gtc()
menu_pop = gtc() menu_pop = gtc()
menu_listbox = gtc() menu_listbox = gtc()
copy_title = gtc() copy_title = gtc()
copy_name = gtc() copy_name = gtc()
enable_remote = gtc() enable_remote = gtc()
disable_remote = gtc() disable_remote = gtc()
remove = gtc() remove = gtc()
def enable_remote_handler(self, *args): def enable_remote_handler(self, *args):
if not self.remote.disabled: if not self.remote.disabled:
self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), _("Remote is already enabled")).toast) self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), _("Remote is already enabled")).toast)
return return
has_error = [] has_error = []
def thread(*args): def thread(*args):
cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--enable', self.remote.name] cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--enable', self.remote.name]
if self.installation == "user" or self.installation == "system": if self.installation == "user" or self.installation == "system":
cmd.append(f"--{self.installation}") cmd.append(f"--{self.installation}")
else: else:
cmd.append(f"--installation={self.installation}") cmd.append(f"--installation={self.installation}")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
has_error.append(str(cpe.stderr)) has_error.append(str(cpe.stderr))
except Exception as e: except Exception as e:
has_error.append(str(e)) has_error.append(str(e))
def callback(*args): def callback(*args):
if len(has_error) > 0: if len(has_error) > 0:
GLib.idle_add(lambda *args, cpe=cpe: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), has_error[0]).toast)) GLib.idle_add(lambda *args, cpe=cpe: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), has_error[0]).toast))
return return
self.remove_css_class("warning") self.remove_css_class("warning")
self.set_icon_name("") self.set_icon_name("")
self.set_tooltip_text("") self.set_tooltip_text("")
self.remote.disabled = False self.remote.disabled = False
self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Enabled remote"))) 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(2).set_visible(False)
self.menu_listbox.get_row_at_index(3).set_visible(True) self.menu_listbox.get_row_at_index(3).set_visible(True)
self.parent_page.total_disabled -= 1 self.parent_page.total_disabled -= 1
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
install_page.start_loading() install_page.start_loading()
install_page.end_loading() install_page.end_loading()
filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page
filters_page.generate_filters() filters_page.generate_filters()
if self.parent_page.total_disabled == 0: if self.parent_page.total_disabled == 0:
self.parent_page.show_disabled_button.set_active(False) self.parent_page.show_disabled_button.set_active(False)
self.parent_page.show_disabled_button.set_visible(False) self.parent_page.show_disabled_button.set_visible(False)
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
def disable_remote_handler(self, *args): def disable_remote_handler(self, *args):
error = [None] error = [None]
def callback(*args): def callback(*args):
if error[0]: if error[0]:
return return
self.add_css_class("warning") self.add_css_class("warning")
self.set_icon_name("error-symbolic") self.set_icon_name("error-symbolic")
self.set_tooltip_text(_("Remote is Disabled")) self.set_tooltip_text(_("Remote is Disabled"))
self.remote.disabled = True self.remote.disabled = True
self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Disabled remote"))) 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(2).set_visible(True)
self.menu_listbox.get_row_at_index(3).set_visible(False) self.menu_listbox.get_row_at_index(3).set_visible(False)
self.set_visible(self.parent_page.show_disabled_button.get_active()) self.set_visible(self.parent_page.show_disabled_button.get_active())
self.parent_page.show_disabled_button.set_visible(True) self.parent_page.show_disabled_button.set_visible(True)
self.parent_page.total_disabled += 1 self.parent_page.total_disabled += 1
self.parent_page.none_visible_handler() self.parent_page.none_visible_handler()
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
install_page.start_loading() install_page.start_loading()
install_page.end_loading() install_page.end_loading()
filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page
filters_page.settings.reset("remotes-list") filters_page.settings.reset("remotes-list")
filters_page.all_remotes_switch.set_active(False) filters_page.all_remotes_switch.set_active(False)
filters_page.generate_filters() filters_page.generate_filters()
filters_page.packages_page.apply_filters() filters_page.packages_page.apply_filters()
def thread(*args): def thread(*args):
if self.remote.disabled: if self.remote.disabled:
self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), _("Remote is already disabled")).toast) self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), _("Remote is already disabled")).toast)
return return
cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--disable', self.remote.name] cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-modify', '--disable', self.remote.name]
if self.installation == "user" or self.installation == "system": if self.installation == "user" or self.installation == "system":
cmd.append(f"--{self.installation}") cmd.append(f"--{self.installation}")
else: else:
cmd.append(f"--installation={self.installation}") cmd.append(f"--installation={self.installation}")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
GLib.idle_add(lambda *args, cpe=cpe: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not disable remote"), str(cpe.stderr)).toast)) 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 error[0] = cpe
return return
except Exception as e: 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)) 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 error[0] = e
return return
def on_response(_, response): def on_response(_, response):
if response != "continue": if response != "continue":
return return
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
dialog = Adw.AlertDialog(heading=_("Disable {}?").format(self.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(self.remote.name)) dialog = Adw.AlertDialog(heading=_("Disable {}?").format(self.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(self.remote.name))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Disable")) dialog.add_response("continue", _("Disable"))
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(self.parent_page.main_window) dialog.present(self.parent_page.main_window)
def on_menu_action(self, listbox, row): def on_menu_action(self, listbox, row):
row = row.get_child() row = row.get_child()
match row: match row:
case self.copy_title: case self.copy_title:
HostInfo.clipboard.set(self.remote.title) HostInfo.clipboard.set(self.remote.title)
self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied title"))) self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied title")))
case self.copy_name: case self.copy_name:
HostInfo.clipboard.set(self.remote.name) HostInfo.clipboard.set(self.remote.name)
self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied name"))) self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Copied name")))
case self.enable_remote: case self.enable_remote:
self.enable_remote_handler() self.enable_remote_handler()
case self.disable_remote: case self.disable_remote:
self.disable_remote_handler() self.disable_remote_handler()
case self.remove: case self.remove:
self.parent_page.remove_remote(self) self.parent_page.remove_remote(self)
self.menu_pop.popdown() self.menu_pop.popdown()
def idle_stuff(self): def idle_stuff(self):
self.set_title(self.remote.title) self.set_title(self.remote.title)
self.set_subtitle(_("Installation: {}").format(self.installation)) self.set_subtitle(_("Installation: {}").format(self.installation))
self.suffix_label.set_label(self.remote.name) self.suffix_label.set_label(self.remote.name)
if self.remote.disabled: if self.remote.disabled:
self.set_icon_name("error-symbolic") self.set_icon_name("error-symbolic")
self.add_css_class("warning") self.add_css_class("warning")
self.set_tooltip_text(_("Remote is Disabled")) self.set_tooltip_text(_("Remote is Disabled"))
def __init__(self, parent_page, installation, remote, **kwargs): def __init__(self, parent_page, installation, remote, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.parent_page = parent_page self.parent_page = parent_page
self.remote = remote self.remote = remote
self.installation = installation self.installation = installation
# Apply # Apply
GLib.idle_add(lambda *_: self.idle_stuff()) GLib.idle_add(lambda *_: self.idle_stuff())
## Show / Hide the Enable / Disable actions depending on remote status ## 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(2).set_visible(remote.disabled)
self.menu_listbox.get_row_at_index(3).set_visible(not remote.disabled) self.menu_listbox.get_row_at_index(3).set_visible(not remote.disabled)
# Connections # Connections
self.menu_listbox.connect("row-activated", self.on_menu_action) self.menu_listbox.connect("row-activated", self.on_menu_action)
self.filter_button.connect("clicked", lambda *_: parent_page.filter_remote(self)) self.filter_button.connect("clicked", lambda *_: parent_page.filter_remote(self))

View File

@@ -2,160 +2,160 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $RemotesPage : Adw.NavigationPage { template $RemotesPage : Adw.NavigationPage {
title: _("Manage Remotes"); title: _("Manage Remotes");
Adw.ToastOverlay toast_overlay { Adw.ToastOverlay toast_overlay {
Stack status_stack { Stack status_stack {
Adw.ToolbarView loading_view { Adw.ToolbarView loading_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.ToolbarView adding_view { Adw.ToolbarView adding_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.ToolbarView main_view { Adw.ToolbarView main_view {
[top] [top]
Adw.HeaderBar header_bar { Adw.HeaderBar header_bar {
[start] [start]
$SidebarButton {} $SidebarButton {}
[start] [start]
ToggleButton search_button { ToggleButton search_button {
icon-name: "loupe-large-symbolic"; icon-name: "loupe-large-symbolic";
tooltip-text: _("Search Packages"); tooltip-text: _("Search Packages");
} }
} }
[top] [top]
Adw.Clamp { Adw.Clamp {
SearchBar search_bar { SearchBar search_bar {
search-mode-enabled: bind search_button.active bidirectional; search-mode-enabled: bind search_button.active bidirectional;
key-capture-widget: template; key-capture-widget: template;
SearchEntry search_entry { SearchEntry search_entry {
hexpand: true; hexpand: true;
placeholder-text: _("Search Remotes"); placeholder-text: _("Search Remotes");
} }
} }
} }
Stack stack { Stack stack {
Adw.PreferencesPage content_page { Adw.PreferencesPage content_page {
Adw.PreferencesGroup current_remotes_group { Adw.PreferencesGroup current_remotes_group {
title: _("Current Remotes"); title: _("Current Remotes");
description: _("Remotes available on your system"); description: _("Remotes available on your system");
header-suffix: header-suffix:
ToggleButton show_disabled_button { ToggleButton show_disabled_button {
valign: center; valign: center;
styles ["flat"] styles ["flat"]
Adw.ButtonContent show_disabled_button_content { Adw.ButtonContent show_disabled_button_content {
icon-name: "eye-not-looking-symbolic"; icon-name: "eye-not-looking-symbolic";
label: _("Show Disabled"); label: _("Show Disabled");
} }
} }
; ;
Adw.ActionRow none_visible { Adw.ActionRow none_visible {
styles ["warning"] styles ["warning"]
[child] [child]
Box { Box {
spacing: 3; spacing: 3;
orientation: vertical; orientation: vertical;
Box { Box {
halign: center; halign: center;
Image { Image {
valign: center; valign: center;
margin-top: 7; margin-top: 7;
margin-end: 6; margin-end: 6;
icon-name: "eye-not-looking-symbolic"; icon-name: "eye-not-looking-symbolic";
} }
Label { Label {
margin-top: 7; margin-top: 7;
label: _("No Enabled Remotes"); label: _("No Enabled Remotes");
wrap: true; wrap: true;
styles ["heading"] styles ["heading"]
} }
} }
Label { Label {
label: _("You only have disabled remotes on this system"); label: _("You only have disabled remotes on this system");
margin-start: 16; margin-start: 16;
margin-end: 16; margin-end: 16;
margin-bottom: 8; margin-bottom: 8;
justify: center; justify: center;
halign: center; halign: center;
wrap: true; wrap: true;
} }
} }
} }
Adw.ActionRow no_remotes { Adw.ActionRow no_remotes {
styles ["error"] styles ["error"]
[child] [child]
Box { Box {
spacing: 3; spacing: 3;
orientation: vertical; orientation: vertical;
Box { Box {
halign: center; halign: center;
Image { Image {
valign: center; valign: center;
margin-top: 7; margin-top: 7;
margin-end: 6; margin-end: 6;
icon-name: "error-symbolic"; icon-name: "error-symbolic";
} }
Label { Label {
margin-top: 7; margin-top: 7;
label: _("No Remotes Found"); label: _("No Remotes Found");
wrap: true; wrap: true;
styles ["heading"] styles ["heading"]
} }
} }
Label { Label {
label: _("Warehouse cannot see the current remotes or your system has no remotes added"); label: _("Warehouse cannot see the current remotes or your system has no remotes added");
margin-start: 16; margin-start: 16;
margin-end: 16; margin-end: 16;
margin-bottom: 8; margin-bottom: 8;
justify: center; justify: center;
halign: center; halign: center;
wrap: true; wrap: true;
} }
} }
} }
} }
Adw.PreferencesGroup new_remotes_group { Adw.PreferencesGroup new_remotes_group {
visible: bind search_button.active inverted; visible: bind search_button.active inverted;
title: _("Add Popular Remotes"); title: _("Add Popular Remotes");
description: _("Add new remotes to get more software"); description: _("Add new remotes to get more software");
} }
Adw.PreferencesGroup other_remotes { Adw.PreferencesGroup other_remotes {
visible: bind search_button.active inverted; visible: bind search_button.active inverted;
title: _("Add Other Remotes"); title: _("Add Other Remotes");
Adw.ActionRow file_remote_row { Adw.ActionRow file_remote_row {
activatable: true; activatable: true;
title: _("Add a Repo File"); title: _("Add a Repo File");
subtitle: _("Open a downloaded repo file to add"); subtitle: _("Open a downloaded repo file to add");
[suffix] [suffix]
Image { Image {
icon-name: "plus-large-symbolic"; icon-name: "plus-large-symbolic";
} }
} }
Adw.ActionRow custom_remote_row { Adw.ActionRow custom_remote_row {
activatable: true; activatable: true;
title: _("Add a Custom Remote"); title: _("Add a Custom Remote");
subtitle: _("Manually enter new remote details"); subtitle: _("Manually enter new remote details");
[suffix] [suffix]
Image { Image {
icon-name: "plus-large-symbolic"; icon-name: "plus-large-symbolic";
} }
} }
} }
} }
Adw.StatusPage no_results { Adw.StatusPage no_results {
title: _("No Results Found"); title: _("No Results Found");
description: _("Try a different search"); description: _("Try a different search");
icon-name: "system-search-symbolic"; icon-name: "system-search-symbolic";
} }
} }
} }
} }
} }
} }

View File

@@ -7,284 +7,284 @@ from .loading_status import LoadingStatus
import subprocess import subprocess
class NewRemoteRow(Adw.ActionRow): class NewRemoteRow(Adw.ActionRow):
__gtype_name__ = "NewRemoteRow" __gtype_name__ = "NewRemoteRow"
def idle_stuff(self, *args): def idle_stuff(self, *args):
self.set_title(self.info["title"]) self.set_title(self.info["title"])
self.set_subtitle(self.info["description"]) self.set_subtitle(self.info["description"])
self.add_suffix(Gtk.Image.new_from_icon_name("plus-large-symbolic")) self.add_suffix(Gtk.Image.new_from_icon_name("plus-large-symbolic"))
def __init__(self, info, **kwargs): def __init__(self, info, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.info = info self.info = info
GLib.idle_add(self.idle_stuff) GLib.idle_add(self.idle_stuff)
self.set_activatable(True) self.set_activatable(True)
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remotes_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remotes_page.ui")
class RemotesPage(Adw.NavigationPage): class RemotesPage(Adw.NavigationPage):
# Preselected Remotes # Preselected Remotes
new_remotes = [ new_remotes = [
{ {
"title": "AppCenter", "title": "AppCenter",
"name": "appcenter", "name": "appcenter",
"link": "https://flatpak.elementary.io/repo.flatpakrepo", "link": "https://flatpak.elementary.io/repo.flatpakrepo",
"description": _("The open source, pay-what-you-want app store from elementary") "description": _("The open source, pay-what-you-want app store from elementary")
}, },
{ {
"title": "Flathub", "title": "Flathub",
"name": "flathub", "name": "flathub",
"link": "https://dl.flathub.org/repo/flathub.flatpakrepo", "link": "https://dl.flathub.org/repo/flathub.flatpakrepo",
"description": _("Central repository of Flatpak applications"), "description": _("Central repository of Flatpak applications"),
}, },
{ {
"title": "Flathub beta", "title": "Flathub beta",
"name": "flathub-beta", "name": "flathub-beta",
"link": "https://flathub.org/beta-repo/flathub-beta.flatpakrepo", "link": "https://flathub.org/beta-repo/flathub-beta.flatpakrepo",
"description": _("Beta builds of Flatpak applications"), "description": _("Beta builds of Flatpak applications"),
}, },
{ {
"title": "Fedora", "title": "Fedora",
"name": "fedora", "name": "fedora",
"link": "oci+https://registry.fedoraproject.org", "link": "oci+https://registry.fedoraproject.org",
"description": _("Flatpaks packaged by Fedora Linux"), "description": _("Flatpaks packaged by Fedora Linux"),
}, },
{ {
"title": "GNOME Nightly", "title": "GNOME Nightly",
"name": "gnome-nightly", "name": "gnome-nightly",
"link": "https://nightly.gnome.org/gnome-nightly.flatpakrepo", "link": "https://nightly.gnome.org/gnome-nightly.flatpakrepo",
"description": _("The latest beta GNOME Apps and Runtimes"), "description": _("The latest beta GNOME Apps and Runtimes"),
}, },
{ {
"title": "WebKit Developer SDK", "title": "WebKit Developer SDK",
"name": "webkit-sdk", "name": "webkit-sdk",
"link": "https://software.igalia.com/flatpak-refs/webkit-sdk.flatpakrepo", "link": "https://software.igalia.com/flatpak-refs/webkit-sdk.flatpakrepo",
"description": _("Central repository of the WebKit Developer and Runtime SDK"), "description": _("Central repository of the WebKit Developer and Runtime SDK"),
} }
] ]
__gtype_name__ = 'RemotesPage' __gtype_name__ = 'RemotesPage'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
search_button = gtc() search_button = gtc()
search_bar = gtc() search_bar = gtc()
search_entry = gtc() search_entry = gtc()
toast_overlay = gtc() toast_overlay = gtc()
stack = gtc() stack = gtc()
current_remotes_group = gtc() current_remotes_group = gtc()
show_disabled_button = gtc() show_disabled_button = gtc()
show_disabled_button_content = gtc() show_disabled_button_content = gtc()
new_remotes_group = gtc() new_remotes_group = gtc()
file_remote_row = gtc() file_remote_row = gtc()
custom_remote_row = gtc() custom_remote_row = gtc()
none_visible = gtc() none_visible = gtc()
status_stack = gtc() status_stack = gtc()
loading_view = gtc() loading_view = gtc()
adding_view = gtc() adding_view = gtc()
main_view = gtc() main_view = gtc()
no_results = gtc() no_results = gtc()
no_remotes = gtc() no_remotes = gtc()
content_page = gtc() content_page = gtc()
# Referred to in the main window # Referred to in the main window
# It is used to determine if a new page should be made or not # It is used to determine if a new page should be made or not
# This must be set to the created object from within the class's __init__ method # This must be set to the created object from within the class's __init__ method
instance = None instance = None
page_name = "remotes" page_name = "remotes"
def start_loading(self): def start_loading(self):
self.search_button.set_active(False) self.search_button.set_active(False)
self.status_stack.set_visible_child(self.loading_view) self.status_stack.set_visible_child(self.loading_view)
self.total_disabled = 0 self.total_disabled = 0
for row in self.current_remote_rows: for row in self.current_remote_rows:
self.current_remotes_group.remove(row) self.current_remotes_group.remove(row)
self.current_remote_rows.clear() self.current_remote_rows.clear()
def end_loading(self): def end_loading(self):
show_disabled = self.show_disabled_button.get_active() show_disabled = self.show_disabled_button.get_active()
self.show_disabled_button.set_visible(False) self.show_disabled_button.set_visible(False)
total_visible = 0 total_visible = 0
for installation, remotes in HostInfo.remotes.items(): for installation, remotes in HostInfo.remotes.items():
for remote in remotes: for remote in remotes:
row = RemoteRow(self, installation, remote) row = RemoteRow(self, installation, remote)
self.current_remote_rows.append(row) self.current_remote_rows.append(row)
self.current_remotes_group.add(row) self.current_remotes_group.add(row)
if row.remote.disabled: if row.remote.disabled:
self.total_disabled += 1 self.total_disabled += 1
self.show_disabled_button.set_visible(True) self.show_disabled_button.set_visible(True)
if show_disabled: if show_disabled:
total_visible += 1 total_visible += 1
else: else:
row.set_visible(False) row.set_visible(False)
else: else:
total_visible += 1 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: if len(self.current_remote_rows) == 0:
self.no_remotes.set_visible(True) self.no_remotes.set_visible(True)
self.none_visible.set_visible(False) self.none_visible.set_visible(False)
else: else:
self.no_remotes.set_visible(False) 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): def none_visible_handler(self):
any_visible = False any_visible = False
for row in self.current_remote_rows: for row in self.current_remote_rows:
if row.get_visible(): if row.get_visible():
any_visible = True any_visible = True
break break
self.none_visible.set_visible(not any_visible) self.none_visible.set_visible(not any_visible)
def filter_remote(self, row): def filter_remote(self, row):
self.filter_setting.set_boolean("show-apps", True) self.filter_setting.set_boolean("show-apps", True)
self.filter_setting.set_boolean("show-runtimes", 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.set_string("remotes-list", f"{row.remote.name}<>{row.installation};")
self.filter_setting.reset("runtimes-list") self.filter_setting.reset("runtimes-list")
packages_page = self.main_window.pages[self.main_window.packages_row] packages_page = self.main_window.pages[self.main_window.packages_row]
packages_page.filters_page.generate_filters() packages_page.filters_page.generate_filters()
packages_page.apply_filters() packages_page.apply_filters()
GLib.idle_add(lambda *_: self.main_window.activate_row(self.main_window.packages_row)) GLib.idle_add(lambda *_: self.main_window.activate_row(self.main_window.packages_row))
GLib.idle_add(lambda *args: packages_page.packages_toast_overlay.add_toast(Adw.Toast(title=_("Showing all packages from {}").format(row.remote.title)))) GLib.idle_add(lambda *args: packages_page.packages_toast_overlay.add_toast(Adw.Toast(title=_("Showing all packages from {}").format(row.remote.title))))
def remove_remote(self, row): def remove_remote(self, row):
error = [None] error = [None]
def thread(*args): def thread(*args):
install = row.installation install = row.installation
cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-delete', row.remote.name, '--force'] cmd = ['flatpak-spawn', '--host', 'flatpak', 'remote-delete', row.remote.name, '--force']
if install == "user" or install == "system": if install == "user" or install == "system":
cmd.append(f"--{install}") cmd.append(f"--{install}")
else: else:
cmd.append(f"--installation={install}") cmd.append(f"--installation={install}")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error[0] = cpe.stderr error[0] = cpe.stderr
except Exception as e: except Exception as e:
error[0] = e error[0] = e
def callback(*args): def callback(*args):
if error[0]: if error[0]:
self.toast_overlay.add_toast(ErrorToast(_("Could not remove remote"), str(error[0])).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not remove remote"), str(error[0])).toast)
else: else:
filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page filters_page = HostInfo.main_window.pages[HostInfo.main_window.packages_row].filters_page
filters_page.settings.reset("remotes-list") filters_page.settings.reset("remotes-list")
filters_page.all_remotes_switch.set_active(False) filters_page.all_remotes_switch.set_active(False)
# filters_page.packages_page.apply_filters() # filters_page.packages_page.apply_filters()
self.main_window.refresh_handler() self.main_window.refresh_handler()
self.toast_overlay.add_toast(Adw.Toast(title=_("Removed {}").format(row.remote.title))) self.toast_overlay.add_toast(Adw.Toast(title=_("Removed {}").format(row.remote.title)))
def on_response(_, response): def on_response(_, response):
if response != "continue": if response != "continue":
return return
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
dialog = Adw.AlertDialog(heading=_("Remove {}?").format(row.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(row.remote.name)) dialog = Adw.AlertDialog(heading=_("Remove {}?").format(row.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(row.remote.name))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Remove")) dialog.add_response("continue", _("Remove"))
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(self.main_window) dialog.present(self.main_window)
def on_search(self, entry): def on_search(self, entry):
text = entry.get_text().lower() text = entry.get_text().lower()
total = 0 total = 0
show_disabled = self.show_disabled_button.get_active() show_disabled = self.show_disabled_button.get_active()
for row in self.current_remote_rows: for row in self.current_remote_rows:
title_match = text in row.get_title().lower() title_match = text in row.get_title().lower()
subtitle_match = text in row.get_subtitle().lower() subtitle_match = text in row.get_subtitle().lower()
visible = (title_match or subtitle_match) and (show_disabled or not row.remote.disabled) visible = (title_match or subtitle_match) and (show_disabled or not row.remote.disabled)
total += visible total += visible
row.set_visible(visible) row.set_visible(visible)
if text == "": if text == "":
self.stack.set_visible_child(self.content_page) self.stack.set_visible_child(self.content_page)
return return
self.stack.set_visible_child(self.content_page if total > 0 else self.no_results) self.stack.set_visible_child(self.content_page if total > 0 else self.no_results)
def local_file_handler(self, path): def local_file_handler(self, path):
try: try:
name = path.split("/")[-1].split(".")[0] name = path.split("/")[-1].split(".")[0]
info = { info = {
"title": name.title(), "title": name.title(),
"name": name, "name": name,
"description": "local file", "description": "local file",
"link": path, "link": path,
} }
AddRemoteDialog(self.main_window, self, info).present(self.main_window) AddRemoteDialog(self.main_window, self, info).present(self.main_window)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast)
def file_callback(self, chooser, result): def file_callback(self, chooser, result):
try: try:
file = chooser.open_finish(result) file = chooser.open_finish(result)
path = file.get_path() path = file.get_path()
self.local_file_handler(path) self.local_file_handler(path)
except GLib.GError as ge: except GLib.GError as ge:
if "Dismissed by user" in str(ge): if "Dismissed by user" in str(ge):
return return
self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(ge)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(ge)).toast)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open file"), str(e)).toast)
def add_file_handler(self): def add_file_handler(self):
file_filter = Gtk.FileFilter(name=_("Flatpak Repos")) file_filter = Gtk.FileFilter(name=_("Flatpak Repos"))
file_filter.add_suffix("flatpakrepo") file_filter.add_suffix("flatpakrepo")
filters = Gio.ListStore.new(Gtk.FileFilter) filters = Gio.ListStore.new(Gtk.FileFilter)
filters.append(file_filter) filters.append(file_filter)
file_chooser = Gtk.FileDialog() file_chooser = Gtk.FileDialog()
file_chooser.set_filters(filters) file_chooser.set_filters(filters)
file_chooser.set_default_filter(file_filter) file_chooser.set_default_filter(file_filter)
file_chooser.open(self.main_window, None, self.file_callback) file_chooser.open(self.main_window, None, self.file_callback)
def show_disabled_handler(self, button): def show_disabled_handler(self, button):
show_disabled = button.get_active() 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") self.show_disabled_button_content.set_icon_name("eye-open-negative-filled-symbolic" if show_disabled else "eye-not-looking-symbolic")
total_visible = 0 total_visible = 0
for row in self.current_remote_rows: for row in self.current_remote_rows:
if row.remote.disabled: if row.remote.disabled:
if show_disabled: # show disabled if show_disabled: # show disabled
row.set_visible(True) row.set_visible(True)
total_visible += 1 total_visible += 1
else: else:
row.set_visible(False) row.set_visible(False)
else: else:
total_visible += 1 total_visible += 1
self.none_visible.set_visible(total_visible == 0) self.none_visible.set_visible(total_visible == 0)
def new_custom_handler(self, *args): def new_custom_handler(self, *args):
AddRemoteDialog(self.main_window, self).present(self.main_window) AddRemoteDialog(self.main_window, self).present(self.main_window)
def __init__(self, main_window, **kwargs): def __init__(self, main_window, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.__class__.instance = self self.__class__.instance = self
self.main_window = main_window self.main_window = main_window
self.search_bar.set_key_capture_widget(main_window) self.search_bar.set_key_capture_widget(main_window)
self.current_remote_rows = [] self.current_remote_rows = []
self.filter_setting = Gio.Settings.new("io.github.flattool.Warehouse.filter") self.filter_setting = Gio.Settings.new("io.github.flattool.Warehouse.filter")
self.total_disabled = 0 self.total_disabled = 0
# Connections # Connections
self.file_remote_row.connect("activated", lambda *_: self.add_file_handler()) self.file_remote_row.connect("activated", lambda *_: self.add_file_handler())
self.custom_remote_row.connect("activated", self.new_custom_handler) self.custom_remote_row.connect("activated", self.new_custom_handler)
self.search_entry.connect("search-changed", self.on_search) self.search_entry.connect("search-changed", self.on_search)
self.show_disabled_button.connect("toggled", self.show_disabled_handler) self.show_disabled_button.connect("toggled", self.show_disabled_handler)
# Apply # Apply
self.adding_view.set_content(LoadingStatus(_("Adding Remote"), _("This should only take a moment"))) 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"))) self.loading_view.set_content(LoadingStatus(_("Loading Remotes"), _("This should only take a moment")))
for item in self.new_remotes: for item in self.new_remotes:
row = NewRemoteRow(item) row = NewRemoteRow(item)
row.connect("activated", lambda *_, remote_info=item: AddRemoteDialog(main_window, self, remote_info).present(main_window)) row.connect("activated", lambda *_, remote_info=item: AddRemoteDialog(main_window, self, remote_info).present(main_window))
self.new_remotes_group.add(row) self.new_remotes_group.add(row)

View File

@@ -2,93 +2,93 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $NewSnapshotDialog : Adw.Dialog { template $NewSnapshotDialog : Adw.Dialog {
follows-content-size: true; follows-content-size: true;
Adw.ToastOverlay toast_overlay { Adw.ToastOverlay toast_overlay {
Adw.NavigationPage nav_page { Adw.NavigationPage nav_page {
title: "No Title Set"; title: "No Title Set";
Adw.ToolbarView { Adw.ToolbarView {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
show-start-title-buttons: false; show-start-title-buttons: false;
show-end-title-buttons: false; show-end-title-buttons: false;
[start] [start]
Button list_cancel_button { Button list_cancel_button {
label: _("Cancel"); label: _("Cancel");
} }
[start] [start]
ToggleButton search_button { ToggleButton search_button {
icon-name: "loupe-large-symbolic"; icon-name: "loupe-large-symbolic";
tooltip-text: _("Search Apps"); tooltip-text: _("Search Apps");
} }
[end] [end]
Button create_button { Button create_button {
sensitive: false; sensitive: false;
label: _("Create"); label: _("Create");
styles ["suggested-action"] styles ["suggested-action"]
} }
} }
[top] [top]
Adw.Clamp { Adw.Clamp {
SearchBar search_bar { SearchBar search_bar {
search-mode-enabled: bind search_button.active bidirectional; search-mode-enabled: bind search_button.active bidirectional;
key-capture-widget: template; key-capture-widget: template;
SearchEntry search_entry { SearchEntry search_entry {
hexpand: true; hexpand: true;
placeholder-text: _("Search Apps"); placeholder-text: _("Search Apps");
} }
} }
} }
Stack stack { Stack stack {
ScrolledWindow scrolled_window { ScrolledWindow scrolled_window {
propagate-natural-height: true; propagate-natural-height: true;
propagate-natural-width: true; propagate-natural-width: true;
Box { Box {
orientation: vertical; orientation: vertical;
Adw.EntryRow name_entry { Adw.EntryRow name_entry {
title: "No Title Set"; title: "No Title Set";
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-top: 12; margin-top: 12;
margin-bottom: 12; margin-bottom: 12;
styles ["card"] styles ["card"]
} }
ListBox listbox { ListBox listbox {
valign: start; valign: start;
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
// margin-top: 12; // margin-top: 12;
margin-bottom: 12; margin-bottom: 12;
selection-mode: none; selection-mode: none;
styles ["boxed-list"] styles ["boxed-list"]
} }
} }
} }
Adw.StatusPage no_results { Adw.StatusPage no_results {
title: _("No Results Found"); title: _("No Results Found");
description: _("Try a different search"); description: _("Try a different search");
icon-name: "system-search-symbolic"; icon-name: "system-search-symbolic";
} }
} }
[bottom] [bottom]
ActionBar { ActionBar {
revealed: bind search_button.visible; revealed: bind search_button.visible;
[start] [start]
Button select_all_button { Button select_all_button {
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
label: _("Select All"); label: _("Select All");
icon-name: "selection-mode-symbolic"; icon-name: "selection-mode-symbolic";
} }
} }
[end] [end]
Label total_selected_label { Label total_selected_label {
label: ""; label: "";
ellipsize: middle; ellipsize: middle;
margin-end: 6; margin-end: 6;
visible: false; visible: false;
} }
} }
} }
} }
} }
} }

View File

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

View File

@@ -2,98 +2,98 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $SnapshotBox : Gtk.Box { template $SnapshotBox : Gtk.Box {
orientation: vertical; orientation: vertical;
spacing: 6; spacing: 6;
Box { Box {
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-top: 6; margin-top: 6;
spacing: 12; spacing: 12;
Box { Box {
orientation: vertical; orientation: vertical;
Label title { Label title {
label: _("No Name Set"); label: _("No Name Set");
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
justify: left; justify: left;
halign: start; halign: start;
styles ["title-4"] styles ["title-4"]
} }
Label date { Label date {
label: _("No date found"); label: _("No date found");
wrap: true; wrap: true;
justify: left; justify: left;
halign: start; halign: start;
} }
} }
Label version { Label version {
label: _("No version found"); label: _("No version found");
wrap: true; wrap: true;
justify: right; justify: right;
hexpand: true; hexpand: true;
halign: end; halign: end;
natural-wrap-mode: none; natural-wrap-mode: none;
} }
} }
Box { Box {
margin-start: 6; margin-start: 6;
margin-end: 6; margin-end: 6;
margin-bottom: 6; margin-bottom: 6;
spacing: 3; spacing: 3;
homogeneous: true; homogeneous: true;
Button apply_button { Button apply_button {
Adw.ButtonContent { Adw.ButtonContent {
label: _("Apply"); label: _("Apply");
icon-name: "check-plain-symbolic"; icon-name: "check-plain-symbolic";
can-shrink: true; can-shrink: true;
} }
hexpand: true; hexpand: true;
styles ["flat"] styles ["flat"]
} }
MenuButton rename_button { MenuButton rename_button {
Adw.ButtonContent { Adw.ButtonContent {
label: _("Rename"); label: _("Rename");
icon-name: "edit-symbolic"; icon-name: "edit-symbolic";
can-shrink: true; can-shrink: true;
} }
hexpand: true; hexpand: true;
styles ["flat"] styles ["flat"]
popover: rename_menu; popover: rename_menu;
} }
Button trash_button { Button trash_button {
Adw.ButtonContent { Adw.ButtonContent {
label: _("Trash"); label: _("Trash");
icon-name: "user-trash-symbolic"; icon-name: "user-trash-symbolic";
can-shrink: true; can-shrink: true;
} }
hexpand: true; hexpand: true;
styles ["flat"] styles ["flat"]
} }
} }
} }
Popover rename_menu { Popover rename_menu {
Box { Box {
orientation: vertical; orientation: vertical;
spacing: 11; spacing: 11;
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-top: 5; margin-top: 5;
margin-bottom: 12; margin-bottom: 12;
Label { Label {
label: _("Rename Snapshot?"); label: _("Rename Snapshot?");
styles ["title-2"] styles ["title-2"]
} }
Box { Box {
spacing: 6; spacing: 6;
Entry rename_entry { Entry rename_entry {
text: bind title.label; text: bind title.label;
} }
Button apply_rename { Button apply_rename {
icon-name: "check-plain-symbolic"; icon-name: "check-plain-symbolic";
tooltip-text: _("Confirm Rename"); tooltip-text: _("Confirm Rename");
styles ["circular", "suggested-action"] styles ["circular", "suggested-action"]
} }
} }
} }
} }

View File

@@ -6,184 +6,184 @@ import os, subprocess, json
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/snapshot_box.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/snapshot_box.ui")
class SnapshotBox(Gtk.Box): class SnapshotBox(Gtk.Box):
__gtype_name__ = "SnapshotBox" __gtype_name__ = "SnapshotBox"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
title = gtc() title = gtc()
date = gtc() date = gtc()
version = gtc() version = gtc()
apply_button = gtc() apply_button = gtc()
rename_button = gtc() rename_button = gtc()
rename_menu = gtc() rename_menu = gtc()
rename_entry = gtc() rename_entry = gtc()
apply_rename = gtc() apply_rename = gtc()
trash_button = gtc() trash_button = gtc()
def create_json(self): def create_json(self):
try: try:
data = { data = {
'snapshot_version': 1, 'snapshot_version': 1,
'name': '', 'name': '',
} }
with open(self.json_path, 'w') as file: with open(self.json_path, 'w') as file:
json.dump(data, file, indent=4) json.dump(data, file, indent=4)
return None return None
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast)
def update_json(self, key, value): def update_json(self, key, value):
try: try:
with open(self.json_path, 'r+') as file: with open(self.json_path, 'r+') as file:
data = json.load(file) data = json.load(file)
data[key] = value data[key] = value
file.seek(0) file.seek(0)
json.dump(data, file, indent=4) json.dump(data, file, indent=4)
file.truncate() file.truncate()
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast)
def load_from_json(self): def load_from_json(self):
if not os.path.exists(self.json_path): if not os.path.exists(self.json_path):
self.create_json() self.create_json()
try: try:
with open(self.json_path, 'r') as file: with open(self.json_path, 'r') as file:
data = json.load(file) data = json.load(file)
name = data['name'] name = data['name']
if name != "": if name != "":
self.title.set_label(GLib.markup_escape_text(name)) self.title.set_label(GLib.markup_escape_text(name))
else: else:
self.title.set_label(_("No Name Set")) self.title.set_label(_("No Name Set"))
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast)
def on_rename(self, widget): def on_rename(self, widget):
if not self.valid_checker(): if not self.valid_checker():
return return
self.update_json('name', self.rename_entry.get_text().strip()) self.update_json('name', self.rename_entry.get_text().strip())
self.load_from_json() self.load_from_json()
self.rename_menu.popdown() self.rename_menu.popdown()
def valid_checker(self, *args): def valid_checker(self, *args):
text = self.rename_entry.get_text().strip() text = self.rename_entry.get_text().strip()
valid = not ("/" in text or "\0" in text) and len(text) > 0 valid = not ("/" in text or "\0" in text) and len(text) > 0
self.apply_rename.set_sensitive(valid) self.apply_rename.set_sensitive(valid)
if valid: if valid:
self.rename_entry.remove_css_class("error") self.rename_entry.remove_css_class("error")
else: else:
self.rename_entry.add_css_class("error") self.rename_entry.add_css_class("error")
return valid return valid
def on_trash(self, button): def on_trash(self, button):
error = [None] error = [None]
path = f"{self.snapshots_path}{self.folder}" path = f"{self.snapshots_path}{self.folder}"
if self.snapshot_page.is_trash_dialog_open: if self.snapshot_page.is_trash_dialog_open:
return return
def thread(*args): def thread(*args):
try: try:
subprocess.run(['gio', 'trash', path], capture_output=True, text=True, check=True) subprocess.run(['gio', 'trash', path], capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error[0] = cpe.stderr error[0] = cpe.stderr
except Exception as e: except Exception as e:
error[0] = str(e) error[0] = str(e)
def callback(*args): def callback(*args):
if not error[0] is None: if not error[0] is None:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshot"), error[0]).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshot"), error[0]).toast)
return return
self.parent_page.on_trash() self.parent_page.on_trash()
self.toast_overlay.add_toast(Adw.Toast.new(_("Trashed snapshot"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Trashed snapshot")))
def on_response(_, response): def on_response(_, response):
self.snapshot_page.is_trash_dialog_open = False self.snapshot_page.is_trash_dialog_open = False
if response != "continue": if response != "continue":
return return
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
self.snapshot_page.is_trash_dialog_open = True self.snapshot_page.is_trash_dialog_open = True
dialog = Adw.AlertDialog(heading=_("Trash Snapshot?"), body=_("This snapshot will be sent to the trash")) dialog = Adw.AlertDialog(heading=_("Trash Snapshot?"), body=_("This snapshot will be sent to the trash"))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Trash")) dialog.add_response("continue", _("Trash"))
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def get_fraction(self): def get_fraction(self):
loading_status = self.snapshot_page.snapshotting_status loading_status = self.snapshot_page.snapshotting_status
loading_status.progress_bar.set_fraction(self.worker.fraction) loading_status.progress_bar.set_fraction(self.worker.fraction)
if self.worker.stop: if self.worker.stop:
self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.split_view) 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) 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 = HostInfo.main_window.pages[HostInfo.main_window.packages_row].properties_page
properties_page.set_properties(properties_page.package, True) properties_page.set_properties(properties_page.package, True)
data_page = HostInfo.main_window.pages[HostInfo.main_window.user_data_row] data_page = HostInfo.main_window.pages[HostInfo.main_window.user_data_row]
data_page.start_loading() data_page.start_loading()
data_page.end_loading() data_page.end_loading()
if self.worker in self.snapshot_page.workers: if self.worker in self.snapshot_page.workers:
self.snapshot_page.workers.remove(self.worker) self.snapshot_page.workers.remove(self.worker)
return False # Stop the timeout return False # Stop the timeout
else: else:
return True # Continue the timeout return True # Continue the timeout
def on_apply(self, button): def on_apply(self, button):
def on_response(dialog, response): def on_response(dialog, response):
if response != "continue": if response != "continue":
return return
self.snapshot_page.snapshotting_status.title_label.set_label(_("Applying Snapshot")) self.snapshot_page.snapshotting_status.title_label.set_label(_("Applying Snapshot"))
self.snapshot_page.snapshotting_status.progress_label.set_visible(False) self.snapshot_page.snapshotting_status.progress_label.set_visible(False)
self.snapshot_page.snapshotting_status.progress_bar.set_fraction(0.0) self.snapshot_page.snapshotting_status.progress_bar.set_fraction(0.0)
self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.snapshotting_view) self.snapshot_page.status_stack.set_visible_child(self.snapshot_page.snapshotting_view)
self.snapshot_page.workers.append(self.worker) self.snapshot_page.workers.append(self.worker)
self.worker.extract() self.worker.extract()
GLib.timeout_add(200, self.get_fraction) GLib.timeout_add(200, self.get_fraction)
has_data = os.path.exists(self.worker.new_path) has_data = os.path.exists(self.worker.new_path)
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Apply Snapshot?"), heading=_("Apply Snapshot?"),
body=_("Any current user data for this app will be trashed") if has_data else "", body=_("Any current user data for this app will be trashed") if has_data else "",
) )
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Apply")) dialog.add_response("continue", _("Apply"))
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def __init__(self, parent_page, folder, snapshots_path, toast_overlay, **kwargs): def __init__(self, parent_page, folder, snapshots_path, toast_overlay, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.snapshot_page = parent_page.parent_page self.snapshot_page = parent_page.parent_page
self.toast_overlay = toast_overlay self.toast_overlay = toast_overlay
self.app_id = snapshots_path.split('/')[-2].strip() self.app_id = snapshots_path.split('/')[-2].strip()
self.worker = TarWorker( self.worker = TarWorker(
existing_path=f"{snapshots_path}{folder}", existing_path=f"{snapshots_path}{folder}",
new_path=f"{HostInfo.home}/.var/app/{self.app_id}/", new_path=f"{HostInfo.home}/.var/app/{self.app_id}/",
toast_overlay=self.toast_overlay, toast_overlay=self.toast_overlay,
) )
split_folder = folder.split('_') split_folder = folder.split('_')
if len(split_folder) < 2: if len(split_folder) < 2:
return return
self.parent_page = parent_page self.parent_page = parent_page
self.folder = folder self.folder = folder
self.snapshots_path = snapshots_path self.snapshots_path = snapshots_path
self.epoch = int(split_folder[0]) self.epoch = int(split_folder[0])
date_data = GLib.DateTime.new_from_unix_local(self.epoch).format("%x %X") date_data = GLib.DateTime.new_from_unix_local(self.epoch).format("%x %X")
self.date.set_label(date_data) self.date.set_label(date_data)
self.version.set_label(_("Version: {}").format(split_folder[1].replace(".tar.zst", ""))) self.version.set_label(_("Version: {}").format(split_folder[1].replace(".tar.zst", "")))
self.json_path = f"{snapshots_path}{folder.replace('tar.zst', 'json')}" self.json_path = f"{snapshots_path}{folder.replace('tar.zst', 'json')}"
self.load_from_json() self.load_from_json()
self.apply_button.connect("clicked", self.on_apply) self.apply_button.connect("clicked", self.on_apply)
self.apply_rename.connect("clicked", self.on_rename) self.apply_rename.connect("clicked", self.on_rename)
self.rename_entry.connect("activate", self.on_rename) self.rename_entry.connect("activate", self.on_rename)
self.rename_entry.connect("changed", self.valid_checker) self.rename_entry.connect("changed", self.valid_checker)
self.trash_button.connect("clicked", self.on_trash) self.trash_button.connect("clicked", self.on_trash)

View File

@@ -2,234 +2,234 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $SnapshotPage : Adw.BreakpointBin { template $SnapshotPage : Adw.BreakpointBin {
width-request: 1; width-request: 1;
height-request: 1; height-request: 1;
Adw.Breakpoint bp1 { Adw.Breakpoint bp1 {
condition ("max-width: 600") condition ("max-width: 600")
setters { setters {
split_view.collapsed: true; split_view.collapsed: true;
split_view.show-content: false; split_view.show-content: false;
} }
} }
Adw.NavigationPage { Adw.NavigationPage {
title: _("Snapshots"); title: _("Snapshots");
Adw.ToastOverlay toast_overlay { Adw.ToastOverlay toast_overlay {
Stack status_stack { Stack status_stack {
Adw.NavigationSplitView split_view { Adw.NavigationSplitView split_view {
sidebar-width-fraction: 0.5; sidebar-width-fraction: 0.5;
max-sidebar-width: 999999999; max-sidebar-width: 999999999;
sidebar: sidebar:
Adw.NavigationPage sidebar_navpage { Adw.NavigationPage sidebar_navpage {
title: _("Snapshots"); title: _("Snapshots");
Adw.ToolbarView sidebar_tbv { Adw.ToolbarView sidebar_tbv {
[top] [top]
Adw.HeaderBar header_bar { Adw.HeaderBar header_bar {
[start] [start]
$SidebarButton {} $SidebarButton {}
[start] [start]
ToggleButton search_button { ToggleButton search_button {
icon-name: "loupe-large-symbolic"; icon-name: "loupe-large-symbolic";
tooltip-text: _("Search Packages"); tooltip-text: _("Search Packages");
} }
[end] [end]
Button new_button { Button new_button {
icon-name: "plus-large-symbolic"; icon-name: "plus-large-symbolic";
tooltip-text: _("New Snapshot"); tooltip-text: _("New Snapshot");
} }
[end] [end]
ToggleButton select_button { ToggleButton select_button {
icon-name: "selection-mode-symbolic"; icon-name: "selection-mode-symbolic";
tooltip-text: _("Select Packages"); tooltip-text: _("Select Packages");
} }
} }
[top] [top]
SearchBar search_bar { SearchBar search_bar {
search-mode-enabled: bind search_button.active bidirectional; search-mode-enabled: bind search_button.active bidirectional;
SearchEntry search_entry { SearchEntry search_entry {
hexpand: true; hexpand: true;
placeholder-text: _("Search Snapshots"); placeholder-text: _("Search Snapshots");
} }
} }
Stack stack { Stack stack {
Adw.StatusPage no_results { Adw.StatusPage no_results {
title: _("No Results Found"); title: _("No Results Found");
description: _("Try a different search"); description: _("Try a different search");
icon-name: "system-search-symbolic"; icon-name: "system-search-symbolic";
} }
ScrolledWindow scrolled_window { ScrolledWindow scrolled_window {
Box { Box {
orientation: vertical; orientation: vertical;
Box active_box { Box active_box {
orientation: vertical; orientation: vertical;
Label { Label {
label: _("Active Snapshots"); label: _("Active Snapshots");
halign: start; halign: start;
styles ["heading"] styles ["heading"]
margin-top: 3; margin-top: 3;
margin-bottom: 6; margin-bottom: 6;
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
} }
Label { Label {
label: _("Snapshots of installed apps"); label: _("Snapshots of installed apps");
halign: start; halign: start;
styles ["dim-label"] styles ["dim-label"]
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-bottom: 3; margin-bottom: 3;
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
} }
ListBox active_listbox { ListBox active_listbox {
styles ["navigation-sidebar"] styles ["navigation-sidebar"]
valign: start; valign: start;
} }
} }
Box leftover_box { Box leftover_box {
orientation: vertical; orientation: vertical;
Label { Label {
label: _("Leftover Snapshots"); label: _("Leftover Snapshots");
halign: start; halign: start;
styles ["heading"] styles ["heading"]
margin-top: 3; margin-top: 3;
margin-bottom: 6; margin-bottom: 6;
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
} }
Label { Label {
label: _("Snapshots of apps that are no longer installed"); label: _("Snapshots of apps that are no longer installed");
halign: start; halign: start;
styles ["dim-label"] styles ["dim-label"]
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-bottom: 3; margin-bottom: 3;
wrap: true; wrap: true;
wrap-mode: word_char; wrap-mode: word_char;
} }
ListBox leftover_listbox { ListBox leftover_listbox {
styles ["navigation-sidebar"] styles ["navigation-sidebar"]
valign: start; valign: start;
} }
} }
} }
} }
} }
[bottom] [bottom]
Revealer { Revealer {
reveal-child: bind select_button.active; reveal-child: bind select_button.active;
transition-type: slide_up; transition-type: slide_up;
[center] [center]
Box bottom_bar { Box bottom_bar {
styles ["toolbar"] styles ["toolbar"]
hexpand: true; hexpand: true;
homogeneous: true; homogeneous: true;
Button select_all_button { Button select_all_button {
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "selection-mode-symbolic"; icon-name: "selection-mode-symbolic";
label: _("Select All"); label: _("Select All");
can-shrink: true; can-shrink: true;
} }
} }
Button copy_button { Button copy_button {
sensitive: false; sensitive: false;
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "edit-copy-symbolic"; icon-name: "edit-copy-symbolic";
label: _("Copy"); label: _("Copy");
can-shrink: true; can-shrink: true;
} }
} }
MenuButton more_button { MenuButton more_button {
sensitive: false; sensitive: false;
popover: more_popover; popover: more_popover;
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "view-more-symbolic"; icon-name: "view-more-symbolic";
label: _("More"); label: _("More");
can-shrink: true; can-shrink: true;
} }
} }
} }
} }
} }
} }
; ;
} }
Adw.ToolbarView no_snapshots { Adw.ToolbarView no_snapshots {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
[start] [start]
Button status_open_button { Button status_open_button {
icon-name: "folder-open-symbolic"; icon-name: "folder-open-symbolic";
tooltip-text: _("Open Snapshots Folder"); tooltip-text: _("Open Snapshots Folder");
} }
} }
Adw.ToastOverlay no_snapshots_toast { Adw.ToastOverlay no_snapshots_toast {
Adw.StatusPage { Adw.StatusPage {
title: _("No Snapshots"); title: _("No Snapshots");
description: _("Create a Snapshot to save the state of any Flatpak application"); description: _("Create a Snapshot to save the state of any Flatpak application");
icon-name: "snapshots-alt-symbolic"; icon-name: "snapshots-alt-symbolic";
Button status_new_button { Button status_new_button {
styles ["suggested-action", "pill"] styles ["suggested-action", "pill"]
halign: center; halign: center;
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "plus-large-symbolic"; icon-name: "plus-large-symbolic";
label: _("New Snapshot"); label: _("New Snapshot");
} }
} }
} }
} }
} }
Adw.ToolbarView loading_view { Adw.ToolbarView loading_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.ToolbarView snapshotting_view { Adw.ToolbarView snapshotting_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
} }
} }
} }
} }
Popover more_popover { Popover more_popover {
styles ["menu"] styles ["menu"]
ListBox more_menu { ListBox more_menu {
Label new_snapshots { Label new_snapshots {
label: _("Snapshot Apps"); label: _("Snapshot Apps");
halign: start; halign: start;
} }
Label install_from_snapshots { Label install_from_snapshots {
label: _("Install Apps"); label: _("Install Apps");
halign: start; halign: start;
} }
Label apply_snapshots { Label apply_snapshots {
label: _("Apply Snapshots"); label: _("Apply Snapshots");
halign: start; halign: start;
} }
Label trash_snapshots { Label trash_snapshots {
label: _("Trash Snapshots"); label: _("Trash Snapshots");
halign: start; halign: start;
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,46 +2,46 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $SnapshotsListPage : Adw.NavigationPage { template $SnapshotsListPage : Adw.NavigationPage {
title: _("Snapshots List"); title: _("Snapshots List");
Adw.ToastOverlay toast_overlay { Adw.ToastOverlay toast_overlay {
Adw.ToolbarView toolbar_view { Adw.ToolbarView toolbar_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
Button open_button { Button open_button {
icon-name: "folder-open-symbolic"; icon-name: "folder-open-symbolic";
tooltip-text: _("Open Snapshots Folder for this App"); tooltip-text: _("Open Snapshots Folder for this App");
} }
} }
ScrolledWindow { ScrolledWindow {
Adw.Clamp { Adw.Clamp {
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-top: 12; margin-top: 12;
margin-bottom: 12; margin-bottom: 12;
ListBox listbox { ListBox listbox {
valign: start; valign: start;
selection-mode: none; selection-mode: none;
styles ["boxed-list"] styles ["boxed-list"]
Adw.PreferencesGroup { Adw.PreferencesGroup {
Adw.ActionRow {title: "test";} Adw.ActionRow {title: "test";}
} }
} }
} }
} }
[bottom] [bottom]
ActionBar { ActionBar {
[center] [center]
Button new_button { Button new_button {
margin-top: 3; margin-top: 3;
margin-bottom: 3; margin-bottom: 3;
styles ["pill", "suggested-action"] styles ["pill", "suggested-action"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "plus-large-symbolic"; icon-name: "plus-large-symbolic";
label: _("New Snapshot"); label: _("New Snapshot");
} }
} }
} }
} }
} }
} }

View File

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

View File

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

View File

@@ -2,83 +2,83 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $DataBox : ListBox { template $DataBox : ListBox {
selection-mode: none; selection-mode: none;
styles ["boxed-list"] styles ["boxed-list"]
Adw.ActionRow row { Adw.ActionRow row {
activatable: bind check_button.visible; activatable: bind check_button.visible;
width-request: 275; width-request: 275;
[child] [child]
Box root_box { Box root_box {
orientation: vertical; orientation: vertical;
Box title_box { Box title_box {
margin-top: 12; margin-top: 12;
margin-bottom: 12; margin-bottom: 12;
Image image { Image image {
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
icon-name: "flatpak-symbolic"; icon-name: "flatpak-symbolic";
icon-size: large; icon-size: large;
} }
Box label_box { Box label_box {
orientation: vertical; orientation: vertical;
Label title_label { Label title_label {
label: "No Title Set"; label: "No Title Set";
hexpand: true; hexpand: true;
halign: start; halign: start;
ellipsize: middle; ellipsize: middle;
margin-end: 12; margin-end: 12;
styles ["title-4"] styles ["title-4"]
} }
Label subtitle_label { Label subtitle_label {
label: "No subtitle set"; label: "No subtitle set";
// hexpand: true; // hexpand: true;
halign: start; halign: start;
ellipsize: middle; ellipsize: middle;
margin-end: 12; margin-end: 12;
} }
} }
} }
Box content_box { Box content_box {
spacing: 6; spacing: 6;
margin-start: 12; margin-start: 12;
margin-end: 6; margin-end: 6;
margin-bottom: 6; margin-bottom: 6;
Spinner spinner { Spinner spinner {
spinning: true; spinning: true;
} }
Label size_label { Label size_label {
label: "No size set"; label: "No size set";
halign: start; halign: start;
hexpand: true; hexpand: true;
} }
Button copy_button { Button copy_button {
icon-name: "copy-symbolic"; icon-name: "copy-symbolic";
tooltip-text: _("Copy Path"); tooltip-text: _("Copy Path");
visible: bind check_button.visible inverted; visible: bind check_button.visible inverted;
styles ["flat", "circular"] styles ["flat", "circular"]
} }
Button open_button { Button open_button {
icon-name: "folder-open-symbolic"; icon-name: "folder-open-symbolic";
tooltip-text: _("Open User Data"); tooltip-text: _("Open User Data");
visible: bind check_button.visible inverted; visible: bind check_button.visible inverted;
styles ["flat", "circular"] styles ["flat", "circular"]
} }
Button install_button { Button install_button {
icon-name: "arrow-pointing-at-line-down-symbolic"; icon-name: "arrow-pointing-at-line-down-symbolic";
tooltip-text: _("Attempt to Install"); tooltip-text: _("Attempt to Install");
styles ["flat", "circular"] styles ["flat", "circular"]
} }
Button trash_button { Button trash_button {
icon-name: "user-trash-symbolic"; icon-name: "user-trash-symbolic";
tooltip-text: _("Trash User Data"); tooltip-text: _("Trash User Data");
visible: bind check_button.visible inverted; visible: bind check_button.visible inverted;
styles ["flat", "circular"] styles ["flat", "circular"]
} }
CheckButton check_button { CheckButton check_button {
visible: false; visible: false;
styles ["selection-mode"] styles ["selection-mode"]
} }
} }
} }
} }
} }

View File

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

View File

@@ -2,72 +2,72 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $DataSubpage : Stack { template $DataSubpage : Stack {
Box content_box { Box content_box {
orientation: vertical; orientation: vertical;
Box label_box { Box label_box {
margin-start: 24; margin-start: 24;
margin-end: 24; margin-end: 24;
halign: fill; halign: fill;
hexpand: true; hexpand: true;
Label title { Label title {
label: _("No Title Set"); label: _("No Title Set");
styles ["title-1"] styles ["title-1"]
hexpand: true; hexpand: true;
justify: fill; justify: fill;
halign: start; halign: start;
wrap: true; wrap: true;
} }
Box subtitle_size_box { Box subtitle_size_box {
Spinner spinner { Spinner spinner {
spinning: true; spinning: true;
valign: center; valign: center;
margin-top: 3; margin-top: 3;
margin-end: 6; margin-end: 6;
} }
Label size_label { Label size_label {
label: _("Loading Size…"); label: _("Loading Size…");
styles ["title-3"] styles ["title-3"]
halign: start; halign: start;
wrap: true; wrap: true;
} }
Label subtitle { Label subtitle {
visible: false; visible: false;
label: "No Subtutle Set"; label: "No Subtutle Set";
styles ["title-3"] styles ["title-3"]
wrap: true; wrap: true;
} }
} }
margin-bottom: 9; margin-bottom: 9;
} }
ScrolledWindow scrolled_window { ScrolledWindow scrolled_window {
vexpand: true; vexpand: true;
Box { Box {
orientation: vertical; orientation: vertical;
Separator { Separator {
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-bottom: 9; margin-bottom: 9;
} }
FlowBox flow_box { FlowBox flow_box {
styles ["boxed-list"] styles ["boxed-list"]
homogeneous: true; homogeneous: true;
valign: start; valign: start;
selection-mode: none; selection-mode: none;
max-children-per-line: 6; max-children-per-line: 6;
margin-start: 12; margin-start: 12;
margin-end: 12; margin-end: 12;
margin-bottom: 12; margin-bottom: 12;
} }
} }
} }
} }
Adw.StatusPage no_data { Adw.StatusPage no_data {
// Contents will be set from the subpage object // Contents will be set from the subpage object
} }
Adw.StatusPage no_results { Adw.StatusPage no_results {
title: _("No Results Found"); title: _("No Results Found");
description: _("Try a different search"); description: _("Try a different search");
icon-name: "system-search-symbolic"; icon-name: "system-search-symbolic";
valign: center; valign: center;
} }
} }

View File

@@ -5,237 +5,237 @@ from .loading_status import LoadingStatus
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/user_data_page/data_subpage.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/user_data_page/data_subpage.ui")
class DataSubpage(Gtk.Stack): class DataSubpage(Gtk.Stack):
__gtype_name__ = 'DataSubpage' __gtype_name__ = 'DataSubpage'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
scrolled_window = gtc() scrolled_window = gtc()
label_box = gtc() label_box = gtc()
subtitle_size_box = gtc() subtitle_size_box = gtc()
title = gtc() title = gtc()
subtitle = gtc() subtitle = gtc()
spinner = gtc() spinner = gtc()
size_label = gtc() size_label = gtc()
flow_box = gtc() flow_box = gtc()
# Statuses # Statuses
content_box = gtc() content_box = gtc()
no_data = gtc() no_data = gtc()
no_results = gtc() no_results = gtc()
def human_readable_size(self): def human_readable_size(self):
working_size = self.total_size working_size = self.total_size
units = ['KB', 'MB', 'GB', 'TB'] units = ['KB', 'MB', 'GB', 'TB']
# size *= 1024 # size *= 1024
for unit in units: for unit in units:
if working_size < 1024: if working_size < 1024:
return f"~ {round(working_size)} {unit}" return f"~ {round(working_size)} {unit}"
working_size /= 1024 working_size /= 1024
return f"~ {round(working_size)} PB" return f"~ {round(working_size)} PB"
def sort_func(self, box1, box2): def sort_func(self, box1, box2):
import random import random
# print(random.randint(1, 100), self.sort_mode, self.sort_ascend) # print(random.randint(1, 100), self.sort_mode, self.sort_ascend)
i1 = None i1 = None
i2 = None i2 = None
if self.sort_mode == "name": if self.sort_mode == "name":
i1 = box1.get_child().title.lower() i1 = box1.get_child().title.lower()
i2 = box2.get_child().title.lower() i2 = box2.get_child().title.lower()
if self.sort_mode == "id": if self.sort_mode == "id":
i1 = box1.get_child().subtitle.lower() i1 = box1.get_child().subtitle.lower()
i2 = box2.get_child().subtitle.lower() i2 = box2.get_child().subtitle.lower()
if self.sort_mode == "size" and self.ready_to_sort_size: if self.sort_mode == "size" and self.ready_to_sort_size:
i1 = box1.get_child().size i1 = box1.get_child().size
i2 = box2.get_child().size i2 = box2.get_child().size
if i1 is None or i2 is None: if i1 is None or i2 is None:
return 0 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): def box_size_callback(self, size):
self.finished_boxes += 1 self.finished_boxes += 1
self.total_size += size self.total_size += size
if self.finished_boxes == self.total_items: if self.finished_boxes == self.total_items:
self.size_label.set_label(self.human_readable_size()) self.size_label.set_label(self.human_readable_size())
self.spinner.set_visible(False) self.spinner.set_visible(False)
self.ready_to_sort_size = True self.ready_to_sort_size = True
if self.sort_mode == "size": if self.sort_mode == "size":
self.flow_box.invalidate_sort() self.flow_box.invalidate_sort()
self.set_visible_child(self.content_box) self.set_visible_child(self.content_box)
GLib.idle_add(lambda *_: self.parent_page.status_stack.set_visible_child(self.parent_page.main_view)) GLib.idle_add(lambda *_: self.parent_page.status_stack.set_visible_child(self.parent_page.main_view))
def trash_handler(self, trashed_box): def trash_handler(self, trashed_box):
self.flow_box.remove(trashed_box) self.flow_box.remove(trashed_box)
if not self.flow_box.get_child_at_index(0): if not self.flow_box.get_child_at_index(0):
self.set_visible_child(self.no_data) self.set_visible_child(self.no_data)
self.parent_page.start_loading() self.parent_page.start_loading()
self.parent_page.end_loading() self.parent_page.end_loading()
def set_selection_mode(self, is_enabled): def set_selection_mode(self, is_enabled):
if not is_enabled: if not is_enabled:
self.size_label.set_visible(True) self.size_label.set_visible(True)
self.subtitle.set_visible(False) self.subtitle.set_visible(False)
idx = 0 idx = 0
while box := self.flow_box.get_child_at_index(idx): while box := self.flow_box.get_child_at_index(idx):
idx += 1 idx += 1
box = box.get_child() box = box.get_child()
if not is_enabled: 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_active(False))
GLib.idle_add(lambda *_, box=box: box.check_button.set_visible(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)) 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): def box_select_handler(self, box):
cb = box.check_button cb = box.check_button
if cb.get_active(): if cb.get_active():
self.selected_boxes.append(box) self.selected_boxes.append(box)
else: else:
try: try:
self.selected_boxes.remove(box) self.selected_boxes.remove(box)
except ValueError: except ValueError:
pass pass
total = len(self.selected_boxes) total = len(self.selected_boxes)
self.subtitle.set_visible(not total == 0) self.subtitle.set_visible(not total == 0)
self.size_label.set_visible(total == 0) self.size_label.set_visible(total == 0)
self.subtitle.set_label(_("{} Selected").format(total)) self.subtitle.set_label(_("{} Selected").format(total))
self.parent_page.copy_button.set_sensitive(total) self.parent_page.copy_button.set_sensitive(total)
self.parent_page.trash_button.set_sensitive(total) self.parent_page.trash_button.set_sensitive(total)
self.parent_page.install_button.set_sensitive(total) self.parent_page.install_button.set_sensitive(total)
self.parent_page.more_button.set_sensitive(total) self.parent_page.more_button.set_sensitive(total)
def box_interact_handler(self, flow_box, box): def box_interact_handler(self, flow_box, box):
box = box.get_child() box = box.get_child()
cb = box.check_button cb = box.check_button
if cb.get_visible(): if cb.get_visible():
cb.set_active(not cb.get_active()) cb.set_active(not cb.get_active())
def select_all_handler(self, *args): def select_all_handler(self, *args):
idx = 0 idx = 0
while box := self.flow_box.get_child_at_index(idx): while box := self.flow_box.get_child_at_index(idx):
idx += 1 idx += 1
box.get_child().check_button.set_active(True) box.get_child().check_button.set_active(True)
def box_rclick_handler(self, box): def box_rclick_handler(self, box):
if self.should_rclick: if self.should_rclick:
self.parent_page.select_button.set_active(True) self.parent_page.select_button.set_active(True)
box.check_button.set_active(not box.check_button.get_active()) box.check_button.set_active(not box.check_button.get_active())
def generate_list(self, flatpaks, data): def generate_list(self, flatpaks, data):
self.flow_box.remove_all() self.flow_box.remove_all()
self.boxes.clear() self.boxes.clear()
self.ready_to_sort_size = False self.ready_to_sort_size = False
self.finished_boxes = 0 self.finished_boxes = 0
self.total_size = 0 self.total_size = 0
self.total_items = len(data) self.total_items = len(data)
self.parent_page.search_entry.set_editable(True) self.parent_page.search_entry.set_editable(True)
self.should_rclick = True self.should_rclick = True
if flatpaks: if flatpaks:
for i, pak in enumerate(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 = 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)) box.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box))
self.boxes.append(box) self.boxes.append(box)
self.flow_box.append(box) self.flow_box.append(box)
else: else:
for i, folder in enumerate(data): 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 = DataBox(self, self.parent_page.toast_overlay, True, folder.split('.')[-1], folder, f"{HostInfo.home}/.var/app/{folder}", None, self.box_size_callback, self.trash_handler)
box.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box)) box.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box))
self.flow_box.append(box) self.flow_box.append(box)
idx = 0 idx = 0
while box := self.flow_box.get_child_at_index(idx): while box := self.flow_box.get_child_at_index(idx):
idx += 1 idx += 1
box.set_focusable(False) box.set_focusable(False)
child = box.get_child() child = box.get_child()
child.set_focusable(False) child.set_focusable(False)
child.row.set_focusable(child.check_button.get_visible()) child.row.set_focusable(child.check_button.get_visible())
rclick = Gtk.GestureClick(button=3) rclick = Gtk.GestureClick(button=3)
rclick.connect("released", lambda *_, child=child: self.box_rclick_handler(child)) rclick.connect("released", lambda *_, child=child: self.box_rclick_handler(child))
box.add_controller(rclick) box.add_controller(rclick)
long_press = Gtk.GestureLongPress() long_press = Gtk.GestureLongPress()
long_press.connect("pressed", lambda *_, child=child: self.box_rclick_handler(child)) long_press.connect("pressed", lambda *_, child=child: self.box_rclick_handler(child))
box.add_controller(long_press) box.add_controller(long_press)
if idx == 0: if idx == 0:
self.set_visible_child(self.no_data) self.set_visible_child(self.no_data)
elif self.sort_mode != "size": elif self.sort_mode != "size":
self.set_visible_child(self.content_box) self.set_visible_child(self.content_box)
self.parent_page.status_stack.set_visible_child(self.parent_page.main_view) self.parent_page.status_stack.set_visible_child(self.parent_page.main_view)
def filter_func(self, box): def filter_func(self, box):
search_text = self.parent_page.search_entry.get_text().lower() search_text = self.parent_page.search_entry.get_text().lower()
box = box.get_child() box = box.get_child()
if search_text in box.title.lower() or search_text in box.subtitle.lower(): if search_text in box.title.lower() or search_text in box.subtitle.lower():
self.is_result = True self.is_result = True
return True return True
def on_invalidate(self, box): def on_invalidate(self, box):
current_status = self.get_visible_child() current_status = self.get_visible_child()
if not current_status is self.no_results: if not current_status is self.no_results:
self.prev_status = self.get_visible_child() self.prev_status = self.get_visible_child()
self.is_result = False self.is_result = False
self.flow_box.invalidate_filter() self.flow_box.invalidate_filter()
if self.is_result: if self.is_result:
self.set_visible_child(self.prev_status) self.set_visible_child(self.prev_status)
else: else:
self.set_visible_child(self.no_results) self.set_visible_child(self.no_results)
if self.parent_page.search_entry.get_text().lower() != "" and self.total_items == 0: if self.parent_page.search_entry.get_text().lower() != "" and self.total_items == 0:
self.set_visible_child(self.no_results) self.set_visible_child(self.no_results)
elif self.total_items == 0: elif self.total_items == 0:
self.set_visible_child(self.no_data) self.set_visible_child(self.no_data)
def update_sort_mode(self): def update_sort_mode(self):
self.sort_ascend = self.settings.get_boolean("sort-ascend") self.sort_ascend = self.settings.get_boolean("sort-ascend")
self.sort_mode = self.settings.get_string("sort-mode") self.sort_mode = self.settings.get_string("sort-mode")
self.flow_box.invalidate_sort() self.flow_box.invalidate_sort()
def __init__(self, title, parent_page, is_active, main_window, **kwargs): def __init__(self, title, parent_page, is_active, main_window, **kwargs):
super().__init__(**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.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.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 # Extra Object Creation
self.main_window = main_window self.main_window = main_window
self.parent_page = parent_page self.parent_page = parent_page
# self.is_active = is_active # self.is_active = is_active
self.sort_mode = "" self.sort_mode = ""
self.sort_ascend = False self.sort_ascend = False
self.total_size = 0 self.total_size = 0
self.total_items = 0 self.total_items = 0
self.boxes = [] self.boxes = []
self.selected_boxes = [] self.selected_boxes = []
self.ready_to_sort_size = False self.ready_to_sort_size = False
self.should_rclick = True self.should_rclick = True
self.finished_boxes = 0 self.finished_boxes = 0
self.is_result = False self.is_result = False
self.prev_status = None self.prev_status = None
self.settings = Gio.Settings.new("io.github.flattool.Warehouse.data_page") self.settings = Gio.Settings.new("io.github.flattool.Warehouse.data_page")
# Apply # Apply
self.flow_box.set_sort_func(self.sort_func) self.flow_box.set_sort_func(self.sort_func)
self.flow_box.set_filter_func(self.filter_func) self.flow_box.set_filter_func(self.filter_func)
if is_active: if is_active:
self.no_data.set_icon_name("error-symbolic") self.no_data.set_icon_name("error-symbolic")
self.no_data.set_title(_("No Active Data")) 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")) self.no_data.set_description(_("Warehouse cannot see any active user data or your system has no active user data present"))
else: else:
self.no_data.set_icon_name("check-plain-symbolic") self.no_data.set_icon_name("check-plain-symbolic")
self.no_data.set_title(_("No Leftover Data")) self.no_data.set_title(_("No Leftover Data"))
self.no_data.set_description(_("There is no leftover user data")) self.no_data.set_description(_("There is no leftover user data"))
# Connections # Connections
parent_page.search_entry.connect("search-changed", self.on_invalidate) parent_page.search_entry.connect("search-changed", self.on_invalidate)
self.flow_box.connect("child-activated", self.box_interact_handler) self.flow_box.connect("child-activated", self.box_interact_handler)

View File

@@ -2,210 +2,210 @@ using Gtk 4.0;
using Adw 1; using Adw 1;
template $UserDataPage : Adw.BreakpointBin { template $UserDataPage : Adw.BreakpointBin {
width-request: 1; width-request: 1;
height-request: 1; height-request: 1;
Adw.Breakpoint bpt { Adw.Breakpoint bpt {
condition ("max-width: 585") condition ("max-width: 585")
setters { setters {
header_bar.title-widget: null; header_bar.title-widget: null;
// header_bar.show-title: false; // header_bar.show-title: false;
switcher_bar.reveal: true; switcher_bar.reveal: true;
switcher_bar.visible: true; switcher_bar.visible: true;
} }
} }
Adw.NavigationPage { Adw.NavigationPage {
title: _("User Data"); title: _("User Data");
Stack status_stack { Stack status_stack {
Adw.ToolbarView loading_view { Adw.ToolbarView loading_view {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
[start] [start]
$SidebarButton {} $SidebarButton {}
} }
} }
Adw.ToolbarView main_view { Adw.ToolbarView main_view {
[top] [top]
Adw.HeaderBar header_bar { Adw.HeaderBar header_bar {
title-widget: title-widget:
Adw.ViewSwitcher { Adw.ViewSwitcher {
stack: stack; stack: stack;
policy: wide; policy: wide;
} }
; ;
[start] [start]
$SidebarButton {} $SidebarButton {}
[start] [start]
ToggleButton search_button { ToggleButton search_button {
icon-name: "system-search-symbolic"; icon-name: "system-search-symbolic";
tooltip-text: _("Search User Data"); tooltip-text: _("Search User Data");
} }
[end] [end]
MenuButton sort_button { MenuButton sort_button {
popover: sort_pop; popover: sort_pop;
icon-name: "vertical-arrows-long-symbolic"; icon-name: "vertical-arrows-long-symbolic";
tooltip-text: _("Sort User Data"); tooltip-text: _("Sort User Data");
} }
[end] [end]
ToggleButton select_button { ToggleButton select_button {
icon-name: "selection-mode-symbolic"; icon-name: "selection-mode-symbolic";
tooltip-text: _("Select User Data"); tooltip-text: _("Select User Data");
} }
} }
[top] [top]
Adw.Clamp { Adw.Clamp {
SearchBar search_bar { SearchBar search_bar {
search-mode-enabled: bind search_button.active bidirectional; search-mode-enabled: bind search_button.active bidirectional;
SearchEntry search_entry { SearchEntry search_entry {
editable: false; editable: false;
hexpand: true; hexpand: true;
placeholder-text: _("Search User Data"); placeholder-text: _("Search User Data");
} }
} }
} }
[bottom] [bottom]
Revealer revealer { Revealer revealer {
reveal-child: bind select_button.active; reveal-child: bind select_button.active;
transition-type: slide_up; transition-type: slide_up;
[center] [center]
Box bottom_bar { Box bottom_bar {
styles ["toolbar"] styles ["toolbar"]
hexpand: true; hexpand: true;
homogeneous: true; homogeneous: true;
Button select_all_button { Button select_all_button {
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "selection-mode-symbolic"; icon-name: "selection-mode-symbolic";
label: _("Select All"); label: _("Select All");
can-shrink: true; can-shrink: true;
} }
} }
Button copy_button { Button copy_button {
sensitive: false; sensitive: false;
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "edit-copy-symbolic"; icon-name: "edit-copy-symbolic";
label: _("Copy"); label: _("Copy");
can-shrink: true; can-shrink: true;
} }
} }
Button install_button { Button install_button {
visible: false; visible: false;
sensitive: false; sensitive: false;
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "arrow-pointing-at-line-down-symbolic"; icon-name: "arrow-pointing-at-line-down-symbolic";
label: _("Install"); label: _("Install");
can-shrink: true; can-shrink: true;
} }
} }
Button trash_button { Button trash_button {
sensitive: false; sensitive: false;
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "user-trash-symbolic"; icon-name: "user-trash-symbolic";
label: _("Trash"); label: _("Trash");
can-shrink: true; can-shrink: true;
} }
} }
MenuButton more_button { MenuButton more_button {
visible: false; visible: false;
sensitive: false; sensitive: false;
popover: more_popover; popover: more_popover;
styles ["raised"] styles ["raised"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "view-more-symbolic"; icon-name: "view-more-symbolic";
label: _("More"); label: _("More");
can-shrink: true; can-shrink: true;
} }
} }
} }
} }
[bottom] [bottom]
Adw.ViewSwitcherBar switcher_bar { Adw.ViewSwitcherBar switcher_bar {
stack: stack; stack: stack;
visible: false; visible: false;
} }
Adw.ToastOverlay toast_overlay { Adw.ToastOverlay toast_overlay {
Adw.ViewStack stack { Adw.ViewStack stack {
} }
} }
} }
} }
} }
} }
Popover more_popover { Popover more_popover {
styles ["menu"] styles ["menu"]
ListBox more_menu { ListBox more_menu {
Label more_install { Label more_install {
label: _("Install"); label: _("Install");
halign: start; halign: start;
} }
Label more_trash { Label more_trash {
label: _("Trash"); label: _("Trash");
halign: start; halign: start;
} }
} }
} }
Popover sort_pop { Popover sort_pop {
styles ["menu"] styles ["menu"]
Box { Box {
orientation: vertical; orientation: vertical;
margin-start: 6; margin-start: 6;
margin-end: 6; margin-end: 6;
margin-top: 6; margin-top: 6;
margin-bottom: 6; margin-bottom: 6;
Box { Box {
homogeneous: true; homogeneous: true;
spacing: 3; spacing: 3;
ToggleButton sort_ascend { ToggleButton sort_ascend {
styles ["flat"] styles ["flat"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "view-sort-ascending-symbolic"; icon-name: "view-sort-ascending-symbolic";
label: _("Ascending"); label: _("Ascending");
} }
} }
ToggleButton sort_descend { ToggleButton sort_descend {
group: sort_ascend; group: sort_ascend;
styles ["flat"] styles ["flat"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "view-sort-descending-symbolic"; icon-name: "view-sort-descending-symbolic";
label: _("Descending"); label: _("Descending");
} }
} }
} }
Separator { Separator {
} }
Box { Box {
homogeneous: true; homogeneous: true;
spacing: 3; spacing: 3;
ToggleButton sort_name { ToggleButton sort_name {
styles ["flat"] styles ["flat"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "font-x-generic-symbolic"; icon-name: "font-x-generic-symbolic";
label: _("Name"); label: _("Name");
} }
} }
ToggleButton sort_id { ToggleButton sort_id {
group: sort_name; group: sort_name;
styles ["flat"] styles ["flat"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "tag-outline-symbolic"; icon-name: "tag-outline-symbolic";
label: _("ID"); label: _("ID");
} }
} }
ToggleButton sort_size { ToggleButton sort_size {
group: sort_name; group: sort_name;
styles ["flat"] styles ["flat"]
Adw.ButtonContent { Adw.ButtonContent {
icon-name: "harddisk-symbolic"; icon-name: "harddisk-symbolic";
label: _("Size"); label: _("Size");
} }
} }
} }
} }
} }

View File

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

View File

@@ -1,85 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<gresources> <gresources>
<gresource prefix="/io/github/flattool/Warehouse"> <gresource prefix="/io/github/flattool/Warehouse">
<file alias="style.css">../data/style.css</file> <file alias="style.css">../data/style.css</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file> <file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
<file preprocess="xml-stripblanks">gtk/loading_status.ui</file> <file preprocess="xml-stripblanks">gtk/loading_status.ui</file>
<file preprocess="xml-stripblanks">gtk/app_row.ui</file> <file preprocess="xml-stripblanks">gtk/app_row.ui</file>
<file preprocess="xml-stripblanks">gtk/installation_chooser.ui</file> <file preprocess="xml-stripblanks">gtk/installation_chooser.ui</file>
<file preprocess="xml-stripblanks">gtk/attempt_install_dialog.ui</file> <file preprocess="xml-stripblanks">gtk/attempt_install_dialog.ui</file>
<file preprocess="xml-stripblanks">main_window/window.ui</file> <file preprocess="xml-stripblanks">main_window/window.ui</file>
<file preprocess="xml-stripblanks">packages_page/packages_page.ui</file> <file preprocess="xml-stripblanks">packages_page/packages_page.ui</file>
<file preprocess="xml-stripblanks">packages_page/filters_page.ui</file> <file preprocess="xml-stripblanks">packages_page/filters_page.ui</file>
<file preprocess="xml-stripblanks">packages_page/uninstall_dialog.ui</file> <file preprocess="xml-stripblanks">packages_page/uninstall_dialog.ui</file>
<file preprocess="xml-stripblanks">properties_page/properties_page.ui</file> <file preprocess="xml-stripblanks">properties_page/properties_page.ui</file>
<file preprocess="xml-stripblanks">change_version_page/change_version_page.ui</file> <file preprocess="xml-stripblanks">change_version_page/change_version_page.ui</file>
<file preprocess="xml-stripblanks">user_data_page/data_box.ui</file> <file preprocess="xml-stripblanks">user_data_page/data_box.ui</file>
<file preprocess="xml-stripblanks">user_data_page/user_data_page.ui</file> <file preprocess="xml-stripblanks">user_data_page/user_data_page.ui</file>
<file preprocess="xml-stripblanks">user_data_page/data_subpage.ui</file> <file preprocess="xml-stripblanks">user_data_page/data_subpage.ui</file>
<file preprocess="xml-stripblanks">remotes_page/remotes_page.ui</file> <file preprocess="xml-stripblanks">remotes_page/remotes_page.ui</file>
<file preprocess="xml-stripblanks">remotes_page/remote_row.ui</file> <file preprocess="xml-stripblanks">remotes_page/remote_row.ui</file>
<file preprocess="xml-stripblanks">remotes_page/add_remote_dialog.ui</file> <file preprocess="xml-stripblanks">remotes_page/add_remote_dialog.ui</file>
<file preprocess="xml-stripblanks">snapshot_page/snapshot_page.ui</file> <file preprocess="xml-stripblanks">snapshot_page/snapshot_page.ui</file>
<file preprocess="xml-stripblanks">snapshot_page/snapshots_list_page.ui</file> <file preprocess="xml-stripblanks">snapshot_page/snapshots_list_page.ui</file>
<file preprocess="xml-stripblanks">snapshot_page/snapshot_box.ui</file> <file preprocess="xml-stripblanks">snapshot_page/snapshot_box.ui</file>
<file preprocess="xml-stripblanks">snapshot_page/new_snapshot_dialog.ui</file> <file preprocess="xml-stripblanks">snapshot_page/new_snapshot_dialog.ui</file>
<file preprocess="xml-stripblanks">install_page/file_install_dialog.ui</file> <file preprocess="xml-stripblanks">install_page/file_install_dialog.ui</file>
<file preprocess="xml-stripblanks">install_page/install_page.ui</file> <file preprocess="xml-stripblanks">install_page/install_page.ui</file>
<file preprocess="xml-stripblanks">install_page/result_row.ui</file> <file preprocess="xml-stripblanks">install_page/result_row.ui</file>
<file preprocess="xml-stripblanks">install_page/select_page.ui</file> <file preprocess="xml-stripblanks">install_page/select_page.ui</file>
<file preprocess="xml-stripblanks">install_page/results_page.ui</file> <file preprocess="xml-stripblanks">install_page/results_page.ui</file>
<file preprocess="xml-stripblanks">install_page/pending_page.ui</file> <file preprocess="xml-stripblanks">install_page/pending_page.ui</file>
<!-- <file preprocess="xml-stripblanks">../data/io.github.flattool.Warehouse.metainfo.xml.in</file> --> <!-- <file preprocess="xml-stripblanks">../data/io.github.flattool.Warehouse.metainfo.xml.in</file> -->
</gresource> </gresource>
<gresource prefix="/io/github/flattool/Warehouse/icons/scalable/actions/"> <gresource prefix="/io/github/flattool/Warehouse/icons/scalable/actions/">
<file preprocess="xml-stripblanks" alias="selection-mode-symbolic.svg">../data/icons/selection-mode-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="selection-mode-symbolic.svg">../data/icons/selection-mode-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="error-symbolic.svg">../data/icons/error-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="error-symbolic.svg">../data/icons/error-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="user-trash-symbolic.svg">../data/icons/user-trash-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="user-trash-symbolic.svg">../data/icons/user-trash-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="folder-visiting-symbolic.svg">../data/icons/folder-visiting-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="folder-visiting-symbolic.svg">../data/icons/folder-visiting-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="info-symbolic.svg">../data/icons/info-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="info-symbolic.svg">../data/icons/info-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="check-plain-symbolic.svg">../data/icons/check-plain-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="check-plain-symbolic.svg">../data/icons/check-plain-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="paper-filled-symbolic.svg">../data/icons/paper-filled-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="paper-filled-symbolic.svg">../data/icons/paper-filled-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="plus-large-symbolic.svg">../data/icons/plus-large-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="plus-large-symbolic.svg">../data/icons/plus-large-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="funnel-symbolic.svg">../data/icons/funnel-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="funnel-symbolic.svg">../data/icons/funnel-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="flatpak-symbolic.svg">../data/icons/flatpak-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="flatpak-symbolic.svg">../data/icons/flatpak-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="right-large-symbolic.svg">../data/icons/right-large-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="right-large-symbolic.svg">../data/icons/right-large-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="view-more-symbolic.svg">../data/icons/view-more-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="view-more-symbolic.svg">../data/icons/view-more-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="clock-alt-symbolic.svg">../data/icons/clock-alt-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="clock-alt-symbolic.svg">../data/icons/clock-alt-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="arrow2-top-right-symbolic.svg">../data/icons/arrow2-top-right-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="arrow2-top-right-symbolic.svg">../data/icons/arrow2-top-right-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="software-update-available-symbolic.svg">../data/icons/software-update-available-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="software-update-available-symbolic.svg">../data/icons/software-update-available-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="software-update-urgent-symbolic.svg">../data/icons/software-update-urgent-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="software-update-urgent-symbolic.svg">../data/icons/software-update-urgent-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="cross-filled-symbolic.svg">../data/icons/cross-filled-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="cross-filled-symbolic.svg">../data/icons/cross-filled-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="important-small-symbolic.svg">../data/icons/important-small-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="important-small-symbolic.svg">../data/icons/important-small-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="eye-not-looking-symbolic.svg">../data/icons/eye-not-looking-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="eye-not-looking-symbolic.svg">../data/icons/eye-not-looking-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="eye-open-negative-filled-symbolic.svg">../data/icons/eye-open-negative-filled-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="eye-open-negative-filled-symbolic.svg">../data/icons/eye-open-negative-filled-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="left-large-symbolic.svg">../data/icons/left-large-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="left-large-symbolic.svg">../data/icons/left-large-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="arrow-turn-left-down-symbolic.svg">../data/icons/arrow-turn-left-down-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="arrow-turn-left-down-symbolic.svg">../data/icons/arrow-turn-left-down-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="arrow-circular-top-right-symbolic.svg">../data/icons/arrow-circular-top-right-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="arrow-circular-top-right-symbolic.svg">../data/icons/arrow-circular-top-right-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="dock-left-symbolic.svg">../data/icons/dock-left-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="dock-left-symbolic.svg">../data/icons/dock-left-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="server-pick-symbolic.svg">../data/icons/server-pick-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="server-pick-symbolic.svg">../data/icons/server-pick-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="file-manager-symbolic.svg">../data/icons/file-manager-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="file-manager-symbolic.svg">../data/icons/file-manager-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="snapshots-alt-symbolic.svg">../data/icons/snapshots-alt-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="snapshots-alt-symbolic.svg">../data/icons/snapshots-alt-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="arrow-pointing-at-line-down-symbolic.svg">../data/icons/arrow-pointing-at-line-down-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="arrow-pointing-at-line-down-symbolic.svg">../data/icons/arrow-pointing-at-line-down-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="loupe-large-symbolic.svg">../data/icons/loupe-large-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="loupe-large-symbolic.svg">../data/icons/loupe-large-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="folder-open-symbolic.svg">../data/icons/folder-open-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="folder-open-symbolic.svg">../data/icons/folder-open-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="padlock2-symbolic.svg">../data/icons/padlock2-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="padlock2-symbolic.svg">../data/icons/padlock2-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="pin-symbolic.svg">../data/icons/pin-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="pin-symbolic.svg">../data/icons/pin-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="pin-small-symbolic.svg">../data/icons/pin-small-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="pin-small-symbolic.svg">../data/icons/pin-small-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="error-small-symbolic.svg">../data/icons/error-small-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="error-small-symbolic.svg">../data/icons/error-small-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="copy-symbolic.svg">../data/icons/copy-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="copy-symbolic.svg">../data/icons/copy-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="double-ended-arrows-vertical-symbolic.svg">../data/icons/double-ended-arrows-vertical-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="double-ended-arrows-vertical-symbolic.svg">../data/icons/double-ended-arrows-vertical-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="vertical-arrows-long-symbolic.svg">../data/icons/vertical-arrows-long-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="vertical-arrows-long-symbolic.svg">../data/icons/vertical-arrows-long-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="dot-symbolic.svg">../data/icons/dot-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="dot-symbolic.svg">../data/icons/dot-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="folder-templates-symbolic.svg">../data/icons/folder-templates-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="folder-templates-symbolic.svg">../data/icons/folder-templates-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="view-sort-ascending-symbolic.svg">../data/icons/view-sort-ascending-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="view-sort-ascending-symbolic.svg">../data/icons/view-sort-ascending-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="view-sort-descending-symbolic.svg">../data/icons/view-sort-descending-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="view-sort-descending-symbolic.svg">../data/icons/view-sort-descending-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="font-x-generic-symbolic.svg">../data/icons/font-x-generic-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="font-x-generic-symbolic.svg">../data/icons/font-x-generic-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="tag-outline-symbolic.svg">../data/icons/tag-outline-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="tag-outline-symbolic.svg">../data/icons/tag-outline-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="harddisk-symbolic.svg">../data/icons/harddisk-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="harddisk-symbolic.svg">../data/icons/harddisk-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="arrow-turn-down-right-symbolic.svg">../data/icons/arrow-turn-down-right-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="arrow-turn-down-right-symbolic.svg">../data/icons/arrow-turn-down-right-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="minus-large-symbolic.svg">../data/icons/minus-large-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="minus-large-symbolic.svg">../data/icons/minus-large-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="view-list-bullet-symbolic.svg">../data/icons/view-list-bullet-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="view-list-bullet-symbolic.svg">../data/icons/view-list-bullet-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="list-remove-all-symbolic.svg">../data/icons/list-remove-all-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="list-remove-all-symbolic.svg">../data/icons/list-remove-all-symbolic.svg</file>
<file preprocess="xml-stripblanks" alias="edit-symbolic.svg">../data/icons/edit-symbolic.svg</file> <file preprocess="xml-stripblanks" alias="edit-symbolic.svg">../data/icons/edit-symbolic.svg</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@@ -36,11 +36,11 @@ locale.textdomain('warehouse')
gettext.install('warehouse', localedir) gettext.install('warehouse', localedir)
if __name__ == '__main__': if __name__ == '__main__':
import gi import gi
from gi.repository import Gio from gi.repository import Gio
resource = Gio.Resource.load(os.path.join(pkgdatadir, 'warehouse.gresource')) resource = Gio.Resource.load(os.path.join(pkgdatadir, 'warehouse.gresource'))
resource._register() resource._register()
from Warehouse import main from Warehouse import main
sys.exit(main.main(VERSION)) sys.exit(main.main(VERSION))

View File

@@ -1,21 +1,21 @@
<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" <Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:gnome="http://api.gnome.org/doap-extensions#" xmlns:gnome="http://api.gnome.org/doap-extensions#"
xmlns="http://usefulinc.com/ns/doap#"> xmlns="http://usefulinc.com/ns/doap#">
<name xml:lang="en">Warehouse</name> <name xml:lang="en">Warehouse</name>
<shortdesc xml:lang="en">Manage all things Flatpak</shortdesc> <shortdesc xml:lang="en">Manage all things Flatpak</shortdesc>
<homepage rdf:resource="https://github.com/flattool/warehouse"/> <homepage rdf:resource="https://github.com/flattool/warehouse"/>
<bug-database rdf:resource="https://github.com/flattool/warehouse/issues"/> <bug-database rdf:resource="https://github.com/flattool/warehouse/issues"/>
<programming-language>Python</programming-language> <programming-language>Python</programming-language>
<platform>GTK 4</platform> <platform>GTK 4</platform>
<platform>Libadwaita</platform> <platform>Libadwaita</platform>
<maintainer> <maintainer>
<foaf:Person> <foaf:Person>
<foaf:name>Heliguy</foaf:name> <foaf:name>Heliguy</foaf:name>
<foaf:mbox rdf:resource="mailto:heliguy4599@hotmail.com"/> <foaf:mbox rdf:resource="mailto:heliguy4599@hotmail.com"/>
</foaf:Person> </foaf:Person>
</maintainer> </maintainer>
</Project> </Project>