Merge pull request #141 from flattool/2.X

Merge 2.X into Main
This commit is contained in:
heliguy4599
2024-10-26 15:45:39 -04:00
committed by GitHub
131 changed files with 9990 additions and 6432 deletions

View File

@@ -2,21 +2,23 @@
![Image banner in the style of GNOME art showing a box truck, tower crane, and storage garage sat on and next to a road.](readme_banner.svg)
## Warehouse is a versatile toolbox for managing flatpak user data, viewing flatpak app info, and batch managing installed flatpaks.
## Warehouse is a versatile toolbox and provides a simple UI to control complex Flatpak options, all without resorting to the command line.
## 🚀 Main Features:
1. **Viewing Flatpak Info:** 📋 Warehouse can display all the information provided by the `flatpak list` command in a user-friendly graphical window. Each item includes a button for easy copying.
1. **Viewing Flatpak Info:** 📋 Warehouse can display all the information provided by the `Flatpak list` command in a user-friendly graphical window. Each item includes a button for easy copying.
2. **Managing User Data:** 🗑 Flatpaks store user data in a specific system location, often left behind when an app is uninstalled. Warehouse can uninstall an app and delete its data, delete data without uninstalling, or simply show if an app has user data.
2. **Change Package Versions:** Rollback any unwanted updates of any package, so long as the remote has older versions.
3. **Batch Actions:** ⚡ Warehouse features a batch mode for swift uninstallations, user data deletions, and app ID copying in bulk.
3. **Managing User Data:** 🗑️ Flatpaks store user data in a specific system location, often left behind when an app is uninstalled. Warehouse can uninstall an app and delete its data, delete data without uninstalling, or simply show if an app has user data.
4. **Leftover Data Management:** 📁 Warehouse scans the user data folder to check for installed apps associated with the data. If none are found, it can delete the data or attempt to install a matching flatpak.
4. **Batch Actions:** Warehouse features a batch mode for swift uninstallations, user data deletions, and app ID copying in bulk.
5. **Manage Remotes:** 📦 Installed and enabled Flatpak remotes can be deleted, and new remotes can be added.
5. **Leftover Data Management:** 📁 Warehouse scans the user data folder to check for installed apps associated with the data. If none are found, it can delete the data or attempt to install a matching Flatpak.
![Various screenshots of Warehouse's abilities](screenshots.png)
6. **Manage Remotes:** 📦 Installed and enabled Flatpak remotes can be deleted, and new remotes can be added.
7. **Make Snapshots:** 🕐 Copy app user data to take quick backups before doing anything risky with your data.
## ⏬ Installation:
@@ -39,13 +41,13 @@ Warehouse is now available on Flathub! Visit your software store and search for
- The Warehouse project follows the [GNOME Code of Conduct](https://conduct.gnome.org/). See `CODE_OF_CONDUCT.md` for more information.
## Important Notes:
- Warehouse assumes flatpak user data is located in the default directory: `~/.var/app`.
- Warehouse does not aim to replace flatpak; it simply facilitates appropriate flatpak commands for the desired actions.
- Warehouse assumes Flatpak user data is located in the default directory: `~/.var/app`.
- Warehouse does not aim to replace Flatpak; it simply facilitates appropriate Flatpak commands for the desired actions.
- This project is still in its early stages, developed by a newcomer. Your understanding of potential bugs is greatly appreciated.
## 🛠️ Installation from Repo Steps:
1. Visit the [releases](https://github.com/flattool/warehouse/releases) page and download `io.github.flattool.Warehouse.flatpak`.
1. Visit the [releases](https://github.com/flattool/warehouse/releases) page and download `io.github.flattool.Warehouse.Flatpak`.
2. Install it using your software store or run the following command:
```shell
flatpak install /path/to/io.github.flattool.Warehouse.flatpak

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 4.003906 8.015625 c 0 0.261719 0.109375 0.515625 0.300782 0.703125 l 3 2.917969 c 0.386718 0.375 1.003906 0.375 1.394531 0 l 3 -2.917969 c 0.394531 -0.386719 0.402343 -1.019531 0.019531 -1.414062 c -0.386719 -0.398438 -1.019531 -0.40625 -1.414062 -0.019532 l -1.304688 1.265625 v -7.550781 c 0 -0.550781 -0.449219 -1 -1 -1 s -1 0.449219 -1 1 v 7.550781 l -1.300781 -1.265625 c -0.398438 -0.386718 -1.03125 -0.378906 -1.414063 0.019532 c -0.1875 0.1875 -0.289062 0.445312 -0.28125 0.710937 z m 0 0" fill-rule="evenodd"/><path d="m 1 15 h 14 v -2 h -14 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 727 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 11.015625 14 c 0.261719 -0.003906 0.515625 -0.113281 0.703125 -0.300781 l 2.917969 -3 c 0.375 -0.390625 0.375 -1.007813 0 -1.398438 l -2.917969 -3 c -0.386719 -0.394531 -1.019531 -0.402343 -1.414062 -0.015625 c -0.398438 0.382813 -0.40625 1.015625 -0.019532 1.414063 l 1.265625 1.300781 h -4.550781 c -1.527344 0 -2.996094 -1.441406 -3 -3 v -4 c 0 -0.550781 -0.449219 -1 -1 -1 s -1 0.449219 -1 1 v 4 c 0.003906 2.683594 2.347656 5 5 5 h 4.550781 l -1.265625 1.300781 c -0.386718 0.398438 -0.378906 1.03125 0.019532 1.417969 c 0.1875 0.183594 0.445312 0.285156 0.710937 0.28125 z m 0 0" fill="#222222" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 766 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 0 3 c 0 -1.644531 1.355469 -3 3 -3 h 5 c 1.644531 0 3 1.355469 3 3 c 0 0.550781 -0.449219 1 -1 1 s -1 -0.449219 -1 -1 c 0 -0.570312 -0.429688 -1 -1 -1 h -5 c -0.570312 0 -1 0.429688 -1 1 v 5 c 0 0.570312 0.429688 1 1 1 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 c -1.644531 0 -3 -1.355469 -3 -3 z m 5 5 c 0 -1.644531 1.355469 -3 3 -3 h 5 c 1.644531 0 3 1.355469 3 3 v 5 c 0 1.644531 -1.355469 3 -3 3 h -5 c -1.644531 0 -3 -1.355469 -3 -3 z m 2 0 v 5 c 0 0.570312 0.429688 1 1 1 h 5 c 0.570312 0 1 -0.429688 1 -1 v -5 c 0 -0.570312 -0.429688 -1 -1 -1 h -5 c -0.570312 0 -1 0.429688 -1 1 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 6.5 14 v -12 h -5 v 12 z m 0 0" fill-opacity="0.35"/><path d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 8 c 0 1.644531 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355469 3 -3 v -8 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 10 c 0.570312 0 1 0.429688 1 1 v 8 c 0 0.570312 -0.429688 1 -1 1 h -10 c -0.570312 0 -1 -0.429688 -1 -1 v -8 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"/><path d="m 6 2 h 1 v 12 h -1 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 11 8 c 0 1.65625 -1.34375 3 -3 3 s -3 -1.34375 -3 -3 s 1.34375 -3 3 -3 s 3 1.34375 3 3 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 12 3.984375 c -0.003906 -0.261719 -0.113281 -0.515625 -0.300781 -0.703125 l -3 -2.917969 c -0.390625 -0.3749998 -1.007813 -0.3749998 -1.394531 0 l -3 2.917969 c -0.398438 0.386719 -0.40625 1.019531 -0.019532 1.414062 c 0.382813 0.398438 1.015625 0.40625 1.414063 0.019532 l 1.300781 -1.265625 v 9.183593 l -1.300781 -1.269531 c -0.398438 -0.382812 -1.03125 -0.375 -1.414063 0.019531 c -0.386718 0.398438 -0.378906 1.03125 0.019532 1.414063 l 3 2.917969 c 0.386718 0.378906 1.003906 0.378906 1.394531 0 l 3 -2.917969 c 0.1875 -0.183594 0.296875 -0.4375 0.300781 -0.703125 c 0.003906 -0.261719 -0.097656 -0.519531 -0.28125 -0.710938 c -0.386719 -0.394531 -1.019531 -0.402343 -1.414062 -0.019531 l -1.304688 1.269531 v -9.183593 l 1.304688 1.265625 c 0.394531 0.386718 1.027343 0.378906 1.414062 -0.019532 c 0.183594 -0.1875 0.285156 -0.445312 0.28125 -0.710937 z m 0 0" fill="#222222" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 12.277344 0.832031 c -0.578125 0.007813 -1.167969 0.230469 -1.691406 0.753907 l -9 9 c -0.375 0.375 -0.585938 0.882812 -0.585938 1.414062 v 3 h 3 c 0.53125 0 1.039062 -0.210938 1.414062 -0.585938 l 9 -9 c 1.789063 -1.789062 0.082032 -4.390624 -1.890624 -4.570312 c -0.082032 -0.011719 -0.164063 -0.011719 -0.246094 -0.011719 z m -1.777344 3.605469 l 1.0625 1.0625 l -7.0625 7.0625 l -1.0625 -1.0625 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8.03125 2.984375 c -2.761719 0 -5 2.238281 -5 5 s 2.238281 5 5 5 s 5 -2.238281 5 -5 s -2.238281 -5 -5 -5 z m -3.03125 4.003906 h 6 c 0.554688 0 1 0.445313 1 1 c 0 0.554688 -0.445312 1 -1 1 h -6 c -0.554688 0 -1 -0.445312 -1 -1 c 0 -0.554687 0.445312 -1 1 -1 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 8 c 1.644531 0 3 -1.355469 3 -3 v -10 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 8 c 0.570312 0 1 0.429688 1 1 v 4 h -10 v -4 c 0 -0.570312 0.429688 -1 1 -1 z m 2.464844 1.429688 c -0.019532 0 -0.039063 0.003906 -0.058594 0.007812 c -0.019531 0 -0.042969 0 -0.0625 0 c -0.214844 0.070312 -0.355469 0.273438 -0.34375 0.5 v 0.0625 c 0 0.546875 0.453125 1 1 1 h 2 c 0.546875 0 1 -0.453125 1 -1 v -0.0625 c 0.011719 -0.675781 -1.011719 -0.675781 -1 0 v 0.0625 h -2 v -0.0625 c 0.003906 -0.296875 -0.246094 -0.527344 -0.535156 -0.507812 z m -3.464844 4.570312 h 10 v 4 c 0 0.570312 -0.429688 1 -1 1 h -8 c -0.570312 0 -1 -0.429688 -1 -1 z m 3.464844 1.429688 c -0.019532 0 -0.039063 0.003906 -0.058594 0.007812 c -0.019531 0 -0.042969 0 -0.0625 0 c -0.214844 0.070312 -0.355469 0.273438 -0.34375 0.5 v 0.0625 c 0 0.546875 0.453125 1 1 1 h 2 c 0.546875 0 1 -0.453125 1 -1 v -0.0625 c 0.011719 -0.675781 -1.011719 -0.675781 -1 0 v 0.0625 h -2 v -0.0625 c 0.003906 -0.296875 -0.246094 -0.527344 -0.535156 -0.507812 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 8 c 0 1.644531 1.355469 3 3 3 h 8.882812 c 0.832032 0 1.578126 -0.402344 2.054688 -0.9375 c 0.472656 -0.53125 0.738281 -1.167969 0.910156 -1.800781 l 0.972656 -2.609375 c 0.390626 -1.449219 -0.09375 -2.652344 -0.820312 -3.167969 c -0.484375 -0.34375 -0.714844 -0.292969 -1 -0.324219 v -1.160156 c 0 -0.855469 -0.558594 -1.589844 -1.09375 -1.828125 c -0.53125 -0.238281 -1.011719 -0.167969 -1.011719 -0.167969 l 0.105469 -0.003906 h -3.585938 l -1.707031 -1.707031 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 2 h 2.585938 l 1.707031 1.707031 c 0.1875 0.1875 0.441406 0.292969 0.707031 0.292969 h 4 c 0.035156 0 0.070312 -0.003906 0.105469 -0.007812 c 0 0 0.019531 0.019531 -0.011719 0.003906 c -0.035156 -0.011719 -0.09375 -0.25 -0.09375 0.003906 v 2 c 0 0.550781 0.449219 1 1 1 c 1 0 1.046875 0.703125 0.886719 1.128906 l -0.972657 2.609375 c -0.117187 0.4375 -0.296874 0.800781 -0.472656 0.996094 c -0.175781 0.199219 -0.285156 0.265625 -0.558594 0.265625 h -8.882812 c -0.570312 0 -1 -0.429688 -1 -1 v -8 c 0 -0.570312 0.46875 -0.792969 1 -1 z m 0 0"/>
<path d="m 7 6 l 0.042969 0.003906 c -0.914063 -0.042968 -1.75 0.390625 -2.195313 0.96875 c -0.710937 1.222656 -1.15625 2.277344 -1.800781 3.71875 c -0.171875 0.523438 0.117187 1.089844 0.640625 1.261719 c 0.527344 0.171875 1.09375 -0.117187 1.261719 -0.640625 c 0.488281 -1.011719 0.921875 -1.816406 1.339843 -2.808594 c 0.210938 -0.503906 0.703126 -0.492187 0.898438 -0.503906 h 5.8125 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 5 1 c -1.644531 0 -3 1.355469 -3 3 v 6.996094 h 1 v -0.996094 h 1 v -6 c 0 -0.570312 0.429688 -1 1 -1 h 4 v 1.5 c 0 1 0.5 1.5 1.5 1.5 h 1.5 v 7 h 1 v 1 h 1 v -8.5 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 l -3.5 -3.5 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 8 13 h -1 v 0.992188 h -1 v 1 h 1 v -0.992188 h 1 z m -2 0.992188 v -1 h -1 v 1 z m -1 0 h -1 v 1 h 1 z m -1 0 v -1 h -1 v 1 z m -1 0 h -1 v 1 h 1 z m -1 0 v -1 h -1 v 1 z m -1 0 h -1 v 1 h 1 z m -1 0 v -1 h -1 v 1 z m -1 0 h -1 v 1 h 1 z m -1 0 v -1 h -1 v 1 z m 0 -1 h 1 v -1 h -1 z m 0 -1 v -1 h -1 v 1 z m 0 -1 h 1 v -1 h -1 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 795 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 10 2 c -0.550781 0 -1 0.449219 -1 1 v 11 c 0 0.550781 0.449219 1 1 1 h 3 c 1.636719 0 2.988281 -1.347656 2.988281 -2.984375 v -2.042969 c 0 -1.636718 -1.351562 -2.984375 -2.988281 -2.984375 h -2 v -3.988281 c 0 -0.550781 -0.449219 -1 -1 -1 z m -8 3 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 h 2 c 0.558594 0 0.992188 0.429688 1 0.988281 h -1.402344 c -1.925781 -0.042969 -3.5429685 1.507813 -3.582031 3.429688 v 0.019531 c 0 1.957031 1.605469 3.5625 3.558594 3.5625 h 2.425781 c 0.550781 0 1 -0.449219 1 -1 v -6 c 0 -1.644531 -1.355469 -3 -3 -3 z m 9 3.988281 h 2 c 0.554688 0 0.988281 0.429688 0.988281 0.984375 v 2.042969 c 0 0.554687 -0.433593 0.984375 -0.988281 0.984375 h -2 z m -7.445312 1 h 0.019531 h 1.425781 v 3.011719 h -1.425781 c -0.867188 0 -1.546875 -0.683594 -1.554688 -1.550781 c 0.023438 -0.835938 0.695313 -1.480469 1.535157 -1.460938 z m 0 0" fill="#2e3434"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 8 c 1.644531 0 3 -1.355469 3 -3 v -10 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 8 c 0.570312 0 1 0.429688 1 1 v 9 c 0 0.570312 -0.429688 1 -1 1 h -8 c -0.554688 0 -1 -0.445312 -1 -1 v -9 c 0 -0.554688 0.445312 -1 1 -1 z m 4 1 c -2.210938 0 -4 1.789062 -4 4 v 4 h 4 c 2.5 0 4 -1.789062 4 -4 s -1.789062 -4 -4 -4 z m 0 2 c 1.105469 0 2 0.894531 2 2 s -0.894531 2 -2 2 s -2 -0.894531 -2 -2 s 0.894531 -2 2 -2 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 646 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 15 1 v 2 h -14 v -2 z m 0 4 v 2 h -14 v -2 z m -7.875 3 c 0.492188 0 0.875 0.382812 0.875 0.875 v 6.25 c 0 0.492188 -0.382812 0.875 -0.875 0.875 h -6.25 c -0.492188 0 -0.875 -0.382812 -0.875 -0.875 v -6.25 c 0 -0.492188 0.382812 -0.875 0.875 -0.875 z m 7.875 1 v 2 h -6 v -2 z m -8.5 2 h -5 s -0.5 0 -0.5 0.5 v 1 c 0 0.5 0.5 0.5 0.5 0.5 h 5 s 0.5 0 0.5 -0.5 v -1 c 0 -0.5 -0.5 -0.5 -0.5 -0.5 z m 8.5 2 v 2 h -6 v -2 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 10.875 10.0625 c -0.8125 0.148438 -1.105469 1.160156 -0.5 1.71875 l 3 3 c 0.957031 0.9375 2.363281 -0.5 1.40625 -1.4375 l -3 -3 c -0.234375 -0.238281 -0.574219 -0.347656 -0.90625 -0.28125 z m 0 0"/><path d="m 6.570312 0.0625 c -3.578124 0 -6.4999995 2.921875 -6.4999995 6.5 s 2.9218755 6.5 6.4999995 6.5 c 3.578126 0 6.5 -2.921875 6.5 -6.5 s -2.921874 -6.5 -6.5 -6.5 z m 0 2 c 2.5 0 4.5 2.003906 4.5 4.5 c 0 2.5 -2 4.5 -4.5 4.5 c -2.496093 0 -4.5 -2 -4.5 -4.5 c 0 -2.496094 2.003907 -4.5 4.5 -4.5 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 1 7 h 14 v 2 h -14 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8 1 c -2.199219 0 -4 1.800781 -4 4 v 2 c -1.109375 0 -2 0.890625 -2 2 v 5 c 0 0.554688 0.445312 1 1 1 h 10 c 0.554688 0 1 -0.445312 1 -1 v -5 c 0 -1.109375 -0.890625 -2 -2 -2 v -2 c 0 -2.199219 -1.800781 -4 -4 -4 z m 0 2 c 1.125 0 2 0.875 2 2 v 2 h -4 v -2 c 0 -1.125 0.875 -2 2 -2 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 9 10 h -2 v 2 l 1 1 l 1 -1 z m 0 0"/><path d="m 10.707031 3.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 h -4 c -0.554688 0 -1 0.449219 -1 1 s 0.445312 1 1 1 h 4 c 0.550781 0 1 -0.449219 1 -1 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 z m 0 0"/><path d="m 4 8.988281 c 0 -2.207031 1.792969 -4 4 -4 s 4 1.792969 4 4 z m 0 0"/><path d="m 6 2.972656 l 0.222656 4.773438 h 3.585938 l 0.191406 -4.738282 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 606 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 9 11 h -2 v 4 l 1 1 l 1 -1 z m 0 0"/><path d="m 12.222656 0.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 h -7.015625 c -0.554688 0 -1 0.449219 -1 1 s 0.445312 1 1 1 h 7.015625 c 0.550781 0 1 -0.449219 1 -1 c 0 -0.265625 -0.105469 -0.519531 -0.292969 -0.707031 z m 0 0"/><path d="m 2 10 c 0 -3.3125 2.6875 -6 6 -6 s 6 2.6875 6 6 z m 0 0"/><path d="m 4.441406 0.972656 l 0.894532 7.164063 h 5.375 l 0.847656 -7.109375 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 612 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 3 0 c -1.109375 0 -2 0.890625 -2 2 v 7 c 0 1.109375 0.890625 2 2 2 h 1 v 1 h -3 v 1 h 8.675781 l 1 -1 h -5.675781 v -1 h 1 c 1.109375 0 2 -0.890625 2 -2 v -7 c 0 -1.109375 -0.890625 -2 -2 -2 z m 8 0 c -1.109375 0 -2 0.890625 -2 2 v 7 c 0 1.109375 0.890625 2 2 2 h 3 c 1.109375 0 2 -0.890625 2 -2 v -7 c 0 -1.109375 -0.890625 -2 -2 -2 z m -8 2 h 3 v 1 h -3 z m 8 0 h 3 v 1 h -3 z m -8 2 h 3 v 5 h -3 z m 8 0 h 3 v 5 h -3 z m -7 1 v 1 h 1 v -1 z m 8 0 v 1 h 1 v -1 z m 0.503906 7 c -0.257812 0 -0.515625 0.097656 -0.710937 0.292969 l -2 2 c -0.183594 0.1875 -0.289063 0.441406 -0.285157 0.707031 h -0.007812 v 1 h 6.003906 v -1 h -0.003906 c 0 -0.265625 -0.101562 -0.519531 -0.289062 -0.707031 l -2 -2 c -0.195313 -0.195313 -0.449219 -0.292969 -0.707032 -0.292969 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 931 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 5 1 v 2.261719 c -1.609375 1.015625 -2.585938 2.785156 -2.585938 4.6875 v 0.050781 h -2.414062 l 7.925781 8.003906 l 8.074219 -8.003906 h -2.492188 c 0 -0.015625 0 -0.03125 0 -0.046875 c 0 -1.871094 -0.941406 -3.617187 -2.507812 -4.640625 v -2.3125 z m 2.867188 2.910156 h 0.09375 c 2.230468 0 4.039062 1.808594 4.039062 4.039063 c 0 2.234375 -1.808594 4.042969 -4.039062 4.042969 c -2.230469 0 -4.039063 -1.808594 -4.042969 -4.042969 c 0 -2.195313 1.753906 -3.988281 3.949219 -4.039063 z m 0 0"/><path d="m 8 4.5 c -0.550781 0 -1 0.449219 -1 1 v 2.863281 l 1.234375 1.46875 c 0.171875 0.207031 0.414063 0.332031 0.679687 0.355469 c 0.265626 0.023438 0.527344 -0.0625 0.730469 -0.230469 c 0.421875 -0.359375 0.476563 -0.988281 0.121094 -1.410156 l -0.765625 -0.910156 v -2.136719 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 6 4.5 c 0 0.828125 -0.671875 1.5 -1.5 1.5 s -1.5 -0.671875 -1.5 -1.5 s 0.671875 -1.5 1.5 -1.5 s 1.5 0.671875 1.5 1.5 z m 0 0"/><path d="m 0 7.089844 c 0 0.847656 0.335938 1.660156 0.9375 2.261718 l 5.382812 5.382813 c 0.929688 0.933594 2.429688 0.933594 3.359376 0 l 4.964843 -4.964844 c 0.980469 -0.980469 0.980469 -2.558593 0 -3.539062 l -5.417969 -5.417969 c -0.519531 -0.519531 -1.226562 -0.8125 -1.960937 -0.8125 h -3.265625 c -2.214844 0 -4 1.785156 -4 4 z m 4 -5.089844 h 3.265625 c 0.207031 0 0.402344 0.082031 0.546875 0.226562 l 5.417969 5.417969 c 0.191406 0.191407 0.191406 0.519531 0 0.710938 l -4.964844 4.964843 c -0.144531 0.144532 -0.386719 0.144532 -0.53125 0 l -5.382813 -5.382812 c -0.226562 -0.226562 -0.351562 -0.53125 -0.351562 -0.847656 v -3.089844 c 0 -1.097656 0.902344 -2 2 -2 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 980 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222" fill-rule="evenodd"><path d="m 1 3.914062 c 0.003906 -0.257812 0.105469 -0.511718 0.304688 -0.703124 l 3 -2.917969 c 0.386718 -0.3789065 1.003906 -0.3789065 1.394531 0 l 3 2.917969 c 0.394531 0.382812 0.402343 1.015624 0.019531 1.414062 c -0.386719 0.394531 -1.019531 0.402344 -1.417969 0.015625 l -1.300781 -1.265625 v 6.550781 c 0 1.332031 -2 1.332031 -2 0 v -6.550781 l -1.300781 1.269531 c -0.398438 0.382813 -1.03125 0.375 -1.414063 -0.019531 c -0.195312 -0.199219 -0.289062 -0.457031 -0.285156 -0.710938 z m 0 0"/><path d="m 7 11.941406 c 0.003906 0.253906 0.105469 0.507813 0.304688 0.703125 l 3 2.917969 c 0.386718 0.375 1.003906 0.375 1.394531 0 l 3 -2.917969 c 0.394531 -0.386719 0.402343 -1.019531 0.019531 -1.414062 c -0.386719 -0.398438 -1.019531 -0.40625 -1.417969 -0.019531 l -1.300781 1.265624 v -6.550781 c 0 -1.332031 -2 -1.332031 -2 0 v 6.550781 l -1.300781 -1.265624 c -0.398438 -0.386719 -1.03125 -0.378907 -1.414063 0.015624 c -0.195312 0.199219 -0.289062 0.457032 -0.285156 0.710938 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 2.5 2.007812 s -0.5 0.222657 -0.5 0.5 v 0.988282 c 0 0.273437 0.5 0.5 0.5 0.5 h 1 s 0.5 -0.226563 0.5 -0.5 v -0.988282 c 0 -0.277343 -0.5 -0.5 -0.5 -0.5 z m 4 0 c -0.277344 0 -0.5 0.222657 -0.5 0.5 v 0.988282 c 0 0.273437 0.222656 0.5 0.5 0.5 h 8 c 0.277344 0 0.5 -0.226563 0.5 -0.5 v -0.988282 c 0 -0.277343 -0.222656 -0.5 -0.5 -0.5 z m -4 5 s -0.5 0.222657 -0.5 0.5 v 0.988282 c 0 0.273437 0.5 0.5 0.5 0.5 h 1 s 0.5 -0.226563 0.5 -0.5 v -0.988282 c 0 -0.277343 -0.5 -0.5 -0.5 -0.5 z m 4 0 s -0.5 0.222657 -0.5 0.5 v 0.988282 c 0 0.273437 0.222656 0.5 0.5 0.5 h 8 c 0.277344 0 0.5 -0.226563 0.5 -0.5 v -0.988282 c 0 -0.277343 -0.5 -0.5 -0.5 -0.5 z m -4 5 c -0.277344 0 -0.5 0.222657 -0.5 0.5 v 0.988282 c 0 0.273437 0.5 0.5 0.5 0.5 h 1 s 0.5 -0.226563 0.5 -0.5 v -0.988282 c 0 -0.277343 -0.222656 -0.5 -0.5 -0.5 z m 4 0 c -0.277344 0 -0.5 0.222657 -0.5 0.5 v 0.988282 c 0 0.273437 0.222656 0.5 0.5 0.5 h 8 c 0.277344 0 0.5 -0.226563 0.5 -0.5 v -0.988282 c 0 -0.277343 -0.222656 -0.5 -0.5 -0.5 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 6.5 6 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 1 c 0 0.277344 0.222656 0.5 0.5 0.5 h 5 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -1 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 0 3 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 1 c 0 0.277344 0.222656 0.5 0.5 0.5 h 7 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -1 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 0 3 c -0.277344 0 -0.5 0.222656 -0.5 0.5 v 1 c 0 0.277344 0.222656 0.5 0.5 0.5 h 9 c 0.277344 0 0.5 -0.222656 0.5 -0.5 v -1 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 0 0"/>
<path d="m 3 16 c -0.550781 0 -1 -0.449219 -1 -1 v -11 h -2 v -1 h 0.0078125 c -0.00390625 -0.265625 0.1015625 -0.519531 0.2851565 -0.707031 l 2 -2 c 0.390625 -0.3906252 1.023437 -0.3906252 1.414062 0 l 2 2 c 0.183594 0.1875 0.289063 0.441406 0.289063 0.707031 h 0.003906 v 1 h -2 v 11 c 0 0.550781 -0.449219 1 -1 1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 3 0 c -0.550781 0 -1 0.449219 -1 1 v 11 h -2 v 1 h 0.0078125 c -0.00390625 0.265625 0.1015625 0.519531 0.2851565 0.707031 l 2 2 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 l 2 -2 c 0.183594 -0.1875 0.289063 -0.441406 0.289063 -0.707031 h 0.003906 v -1 h -2 v -11 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0"/>
<path d="m 6.5 10 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 h 5 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 z m 0 -3 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 h 7 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 z m 0 -3 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 h 9 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -13,6 +13,12 @@
<key name="is-fullscreen" type="b">
<default>false</default>
</key>
<key name="sidebar-shown" type="b">
<default>true</default>
</key>
<key name="page-shown" type="s">
<default>"packages"</default>
</key>
</schema>
<schema id="io.github.flattool.Warehouse.filter" path="/io/github/flattool/Warehouse/filter/">
<key name="show-apps" type="b">
@@ -28,4 +34,12 @@
<default>"all"</default>
</key>
</schema>
<schema id="io.github.flattool.Warehouse.data_page" path="/io/github/flattool/Warehouse/data_page/">
<key name="sort-ascend" type="b">
<default>false</default>
</key>
<key name="sort-mode" type="s">
<default>"size"</default>
</key>
</schema>
</schemalist>

View File

@@ -8,26 +8,29 @@
<project_license>GPL-3.0-only</project_license>
<summary>Manage all things Flatpak</summary>
<description>
<p>Warehouse is an app that manages installed Flatpaks, their user data, and Flatpak remotes.</p>
<p>Warehouse provides a simple UI to control complex Flatpak options, all without resorting to the command line.</p>
<p>Features:</p>
<ul>
<li>Show and filter the list of installed Flatpaks</li>
<li>Display properties of installed Flatpaks</li>
<li>Manage large groups of Flatpaks at once</li>
<li>Add and remove Flatpak remotes</li>
<li>Find and trash leftover user data</li>
<li>Reinstall apps that have leftover data</li>
</ul>
<ul>
<li>Manage installed Flatpaks and view properties of any package</li>
<li>Change versions of a Flatpak to rollback any unwanted updates</li>
<li>Pin runtimes and mask Flatpaks</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>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>Install new packages from any remote, or from your system</li>
<li>Responsive UI to fit large and small screen sizes</li>
</ul>
</description>
<branding>
<color type="primary" scheme_preference="light">#AECEF4</color>
<color type="primary" scheme_preference="dark">#072F5E</color>
</branding>
<recommends>
<supports>
<control>keyboard</control>
<control>pointing</control>
<control>touch</control>
</recommends>
</supports>
<requires>
<display_length compare="ge">330</display_length>
</requires>
@@ -39,54 +42,68 @@
<url type="donation">https://ko-fi.com/heliguy</url>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/MainView.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>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/BatchMode.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>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/Properties.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>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/Remotes.png</image>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/data_page_wide.png</image>
<caption>Manage Apps' User Data</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/SearchInstall.png</image>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/snapshots_page_wide.png</image>
<caption>Backup Apps' User Data</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/Snapshots.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>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/Orphans.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/flattool/warehouse/main/app_page_screeshots/Downgrade.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>
</screenshot>
</screenshots>
<releases>
<release version="1.6.5" date="2024-9-18" timestamp="1726790850">
<release version="2.0.0" date="2024-10-28" timestamp="1730014216">
<description translate="no">
<p>New Features and Changes</p>
<p>New Features</p>
<ul>
<li>Bump GNOME runtime version to 47</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 smaller window sizes</li>
<li>Improved UI for installing packages from remotes</li>
<li>Snapshots can be given names</li>
<li>Packages can be reinstalled with a few clicks</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>Custom installation locations are now supported</li>
<li>Leftover Snapshots are now shown</li>
<li>Apps can be reinstalled from leftover Snapshots</li>
<li>Installation location of disabled remotes is now shown</li>
</ul>
<p>Bug Fixes</p>
<p>Changes</p>
<ul>
<li>Fix issue causing crash on startup due to Flatpaks with multi-line descriptions</li>
<li>Fix Properties Window's layout when a long package name is shown</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>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>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>Refreshing now shows a loading animation</li>
</ul>
</description>
</release>
<release version="1.6.4" date="2024-7-6" timestamp="1720282600">
<description translate="no">
<p>Bux Fixes</p>
<p>Bug Fixes and Performance Improvements</p>
<ul>
<li>Fix issue causing downgrade window to not be able to downgrade anything</li>
</ul>
<p>Previous Releases's Bug Fixes</p>
<ul>
<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>Warehouse is now faster to open</li>
<li>Getting system information is now faster</li>
<li>Long running processes no longer freeze the app</li>
<li>Refreshing is no longer possible when long running processes are happening</li>
</ul>
</description>
</release>

View File

@@ -0,0 +1,10 @@
/* Thanks, TheEvilSkeleton! */
.drag-overlay-status-page {
background-color: alpha(var(--accent-bg-color), 0.5);
color: var(--accent-fg-color);
padding: 32px;
}
.blurred {
filter: blur(6px);
}

View File

@@ -1,84 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $DowngradeWindow: Adw.Dialog {
content-width: 500;
content-height: 450;
Adw.ToolbarView main_toolbar_view {
[top]
Adw.HeaderBar header_bar {
}
[bottom]
ActionBar action_bar {
revealed: false;
[center]
Button apply_button {
visible: false;
valign: end;
halign: center;
margin-top: 6;
margin-bottom: 6;
Adw.ButtonContent {
label: _("Downgrade");
icon-name: "arrow-turn-left-down-symbolic";
}
styles [
"suggested-action",
"pill"
]
}
}
content: Adw.ToastOverlay toast_overlay {
Stack main_stack {
Box loading {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label loading_label {
label: _("Fetching Releases");
styles [
"title-1",
"title"
]
}
Label {
label: _("This could take a while");
styles ["description", "body"]
}
}
Adw.PreferencesPage outerbox {
Adw.PreferencesGroup {
Adw.SwitchRow mask_row {
title: _("Disable Updates");
active: true;
}
}
Adw.PreferencesGroup versions_group {
title: _("Select a Release");
description: _("This will uninstall the current release and install the chosen one instead. Note that downgrading can cause issues.");
}
}
}
};
}
}

View File

@@ -1,83 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $FilterWindow: Adw.Dialog {
title: _("Set Filters");
content-width: 500;
content-height: 450;
child:
Adw.ToolbarView main_toolbar_view {
[top]
Adw.HeaderBar header_bar {
}
content: Adw.ToastOverlay toast_overlay {
Stack main_stack {
Overlay main_overlay {
ScrolledWindow scrolled_window {
vexpand: true;
Adw.Clamp {
Box outerbox {
orientation: vertical;
ListBox install_type_list {
margin-top: 12;
margin-bottom: 12;
margin-start: 12;
margin-end: 12;
hexpand: true;
valign: start;
selection-mode: none;
styles [
"boxed-list"
]
Adw.ActionRow apps_row {
title: _("Show Apps");
Switch show_apps_switch {
valign: center;
}
activatable-widget: show_apps_switch;
}
Adw.ActionRow show_runtimes_row {
title: _("Show Runtimes");
Switch show_runtimes_switch {
valign: center;
}
activatable-widget: show_runtimes_switch;
}
Adw.ExpanderRow remotes_expander {
enable-expansion: true;
title: _("Filter by Remotes");
}
Adw.ExpanderRow runtimes_expander {
enable-expansion: true;
title: _("Filter by Runtimes");
}
}
Button reset_button {
visible: true;
margin-bottom: 18;
halign: center;
label: _("Reset Filters");
styles ["pill"]
}
}
}
}
}
}
};
};
}

View File

@@ -1,135 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $OrphansWindow: Adw.Dialog {
content-width: 500;
content-height: 450;
Adw.ToolbarView main_toolbar_view {
[top]
Adw.HeaderBar header_bar {
[start]
ToggleButton search_button {
icon-name: "system-search-symbolic";
tooltip-text: _("Search List");
}
[end]
Button oepn_folder_button {
icon-name: "document-open-symbolic";
tooltip-text: _("Open Data Folder");
}
}
[top]
SearchBar search_bar {
search-mode-enabled: bind search_button.active bidirectional;
key-capture-widget: main_toolbar_view;
Adw.Clamp {
maximum-size: 577;
hexpand: true;
SearchEntry search_entry {}
}
}
content: Adw.ToastOverlay toast_overlay {
Overlay main_overlay {
Stack main_stack {
Box main_box {
orientation: vertical;
ScrolledWindow scrolled_window {
vexpand: true;
Adw.Clamp {
ListBox list_of_data {
margin-top: 12;
margin-bottom: 12;
margin-start: 12;
margin-end: 12;
hexpand: true;
valign: start;
selection-mode: none;
styles [
"boxed-list"
]
}
}
}
}
Box installing {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label {
label: _("Installing");
styles [
"title-1",
"title"
]
}
Label installing_status {
label: "";
justify: center;
styles [
"description",
"body"
]
}
}
Adw.StatusPage no_data {
icon-name: "check-plain-symbolic";
title: _("No Leftover Data");
description: _("There is no leftover user data");
}
Adw.StatusPage no_results {
icon-name: "system-search-symbolic";
title: _("No Results Found");
description: _("Try a different search term");
}
}
}
};
[bottom]
ActionBar action_bar {
[start]
ToggleButton select_all_button {
label: _("Select All");
}
[end]
Button trash_button {
label: _("Trash");
sensitive: false;
}
[end]
Button install_button {
label: _("Install");
sensitive: false;
}
}
}
}

View File

@@ -1,59 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $PopularRemotesWindow: Adw.Window {
default-width: 450;
default-height: 530;
title: "";
Adw.ToolbarView main_toolbar_view {
[top]
HeaderBar header_bar {}
content: Adw.ToastOverlay toast_overlay {
vexpand: true;
Adw.StatusPage {
valign: start;
title: _("Add Remote");
description: _("Choose from a list of popular remotes or add a new one");
Adw.Clamp {
Box {
orientation: vertical;
ListBox list_of_remotes {
hexpand: true;
valign: start;
selection-mode: none;
styles [
"boxed-list"
]
}
ListBox custom_list {
hexpand: true;
valign: start;
selection-mode: none;
styles [
"boxed-list"
]
Adw.ActionRow add_from_file {
title: _("Add a Repo File");
activatable: true;
}
Adw.ActionRow custom_remote {
title: _("Add a Custom Remote");
activatable: true;
}
}
}
}
}
};
}
}

View File

@@ -1,167 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $PropertiesWindow: Adw.Dialog {
content-width: 350;
content-height: 999999;
title: _("Properties");
Adw.ToolbarView main_toolbar_view {
[top]
Adw.HeaderBar header_bar {}
content: Adw.ToastOverlay toast_overlay {
Box {
orientation: vertical;
Adw.Banner eol_app_banner {}
Adw.Banner eol_runtime_banner {}
Adw.Banner mask_banner {}
ScrolledWindow {
Adw.Clamp {
Box {
orientation: vertical;
hexpand: false;
vexpand: true;
spacing: 12;
margin-top: 12;
margin-start: 12;
margin-end: 12;
margin-bottom: 12;
Image app_icon {
pixel-size: 100;
styles [
"icon-dropshadow"
]
}
Label name {
wrap: true;
wrap-mode: word_char;
justify: center;
styles [
"title-1"
]
}
Button description_button {
visible: bind description.visible;
styles [
"title-4",
"flat"
]
Box {
spacing: 12;
Label description {
halign: start;
wrap: true;
hexpand: true;
}
Image {
icon-name: "edit-copy-symbolic";
}
}
}
Adw.PreferencesGroup upper {
Adw.ActionRow data_row {
title: _("Loading User Data");
[suffix]
Button open_data {
icon-name: "document-open-symbolic";
tooltip-text: _("Open User Data Folder");
valign: center;
visible: false;
styles [
"flat"
]
}
[suffix]
Button trash_data {
icon-name: "user-trash-symbolic";
tooltip-text: _("Trash User Data");
valign: center;
visible: false;
styles [
"flat"
]
}
[suffix]
Spinner spinner {
spinning: true;
}
styles["property"]
}
Adw.ActionRow view_apps {
title: _("Show Apps Using This Runtime");
activatable: true;
visible: false;
[suffix]
Image {
icon-name: "funnel-symbolic";
}
}
Adw.ActionRow runtime {
title: _("Runtime");
[suffix]
Button runtime_properties {
icon-name: "info-symbolic";
tooltip-text: _("View Properties");
valign: center;
styles [
"flat"
]
}
[suffix]
Button runtime_copy {
icon-name: "edit-copy-symbolic";
tooltip-text: _("Copy");
valign: center;
styles [
"flat"
]
}
styles["property"]
}
Adw.ActionRow details {
title: _("Show Details in Store");
activatable: true;
[suffix]
Image {
icon-name: "adw-external-link-symbolic";
}
}
}
Adw.PreferencesGroup lower {}
}
}
}
}
};
}
}

View File

@@ -1,95 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $RemotesWindow: Adw.Dialog {
title: _("Manage Remotes");
content-width: 500;
content-height: 450;
Adw.ToolbarView main_toolbar_view {
[top]
Adw.HeaderBar header_bar {
Button refresh {
icon-name: "view-refresh-symbolic";
tooltip-text: _("Refresh List of Remotes");
}
}
content: Adw.ToastOverlay toast_overlay {
Stack stack {
Adw.PreferencesPage main_group {
Adw.PreferencesGroup remotes_list {
title: _("Installed Remotes");
header-suffix: ToggleButton show_disabled_button {
Adw.ButtonContent show_disabled_button_button_content {
icon-name: "eye-not-looking-symbolic";
label: _("Show Disabled");
styles["flat"]
}
// spacing: 6;
// margin-end: 6;
// Label {
// label: _("Show Disabled");
// styles["heading", "h4"]
// }
// Switch show_disabled {
// valign: center;
// }
};
Adw.ActionRow no_remotes {
title: _("No Remotes Found");
}
}
Adw.PreferencesGroup popular_remotes_list {
title: _("Add a Popular Remote");
visible: false;
}
Adw.PreferencesGroup manual_remotes_list {
title: _("Add Other Remotes");
Adw.ActionRow add_from_file {
title: _("Add a Repo File");
activatable: true;
}
Adw.ActionRow custom_remote {
title: _("Add a Custom Remote");
activatable: true;
}
}
}
Box adding {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label {
label: _("Adding Remote");
styles [
"title-1",
"title"
]
}
Label {
label: _("This should only take a moment");
styles ["description", "body"]
}
}
}
};
}
}

View File

@@ -1,193 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $SearchInstallWindow: Adw.Dialog {
content-width: 500;
content-height: 450;
title: "";
Adw.ToolbarView main_toolbar_view {
content:
Stack outer_stack {
Adw.NavigationView nav_view {
Adw.NavigationPage search_page {
title: _("Search Criteria");
Adw.ToastOverlay toast_overlay {
Adw.ToolbarView {
[top]
Adw.HeaderBar {
}
content:
Adw.StatusPage {
title: _("Choose a Remote to Search");
valign: start;
child:
Adw.Clamp {
ListBox remotes_list {
selection-mode: none;
styles ["boxed-list"]
}
};
};
}
}
}
Adw.NavigationPage results_page {
title: _("Results");
Adw.ToolbarView {
[top]
Adw.HeaderBar {
}
[bottom]
ActionBar action_bar {
revealed: false;
[center]
Button install_button {
margin-top: 6;
margin-bottom: 6;
styles[
"pill",
"suggested-action"
]
Adw.ButtonContent {
label: _("Install");
icon-name: "plus-large-symbolic";
}
}
}
[top]
Adw.Clamp {
Box search_box {
margin-top: 4;
margin-start: 12;
margin-end: 12;
margin-bottom: 6;
SearchEntry search_entry {
hexpand: true;
}
Button search_button {
icon-name: "right-large-symbolic";
tooltip-text: _("Start Search");
}
styles ["linked"]
}
}
content:
Stack inner_stack {
Adw.StatusPage blank_page {
title: _("Search for Flatpaks");
icon-name: "flatpak-symbolic";
description: _("Search for Flatpaks that you want to install");
}
Adw.StatusPage no_results {
icon-name: "system-search-symbolic";
title: _("No Results Found");
description: _("Try a different search term");
}
Box loading_page {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label {
label: _("Searching");
styles [
"title-1",
"title"
]
}
}
Adw.StatusPage too_many {
icon-name: "error-symbolic";
title: _("Too Many Results");
description: _("Try being more specific with your search");
}
ScrolledWindow results_scroll {
vexpand: true;
Adw.Clamp {
ListBox results_list {
margin-top: 6;
margin-bottom: 12;
margin-start: 12;
margin-end: 12;
hexpand: true;
valign: start;
selection-mode: none;
styles ["boxed-list"]
}
}
}
};
}
}
}
Adw.ToolbarView installing {
[top]
Adw.HeaderBar {
}
content:
Overlay overlay {
[overlay]
ProgressBar progress_bar {
visible: false;
can-target: false;
styles ["osd"]
}
Box {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label {
label: _("Installing");
styles [
"title-1",
"title"
]
}
Label installing_status {
label: "";
justify: center;
styles [
"description",
"body"
]
}
}
};
}
};
}
}

View File

@@ -1,90 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $SnapshotsWindow: Adw.Dialog {
content-width: 500;
content-height: 455;
Adw.ToolbarView main_toolbar_view {
[top]
Adw.HeaderBar header_bar {
[end]
Button open_folder_button {
icon-name: "document-open-symbolic";
tooltip-text: _("Open Snapshots Folder");
}
}
[bottom]
ActionBar action_bar {
[center]
Button new_snapshot {
halign: center;
sensitive: bind action_bar.revealed;
margin-top: 6;
margin-bottom: 6;
styles[
"pill",
"suggested-action"
]
Adw.ButtonContent {
label: _("New Snapshot");
icon-name: "plus-large-symbolic";
}
}
}
content: Adw.ToastOverlay toast_overlay {
Stack main_stack {
ScrolledWindow outerbox {
Adw.Clamp {
ListBox snapshots_group {
margin-top: 12;
margin-bottom: 12;
margin-start: 12;
margin-end: 12;
valign: start;
selection-mode: none;
styles [
"boxed-list"
]
}
}
}
Box loading {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label loading_label {
styles [
"title-1",
"title"
]
}
}
Adw.StatusPage no_snapshots {
title: _("No Snapshots");
description: _("Snapshots are backups of the app's user data. They can be reapplied at any time.");
icon-name: "clock-alt-symbolic";
}
}
};
}
}

View File

@@ -1,299 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $WarehouseWindow: Adw.ApplicationWindow {
title: "Warehouse";
Adw.ToolbarView main_toolbar_view {
[top]
HeaderBar header_bar {
[start]
ToggleButton search_button {
icon-name: "system-search-symbolic";
tooltip-text: _("Search List");
}
[start]
Button filter_button {
icon-name: "funnel-symbolic";
tooltip-text: _("Filter List");
}
[end]
MenuButton main_menu {
icon-name: "open-menu-symbolic";
tooltip-text: _("Main Menu");
menu-model: primary_menu;
}
[end]
ToggleButton batch_mode_button {
icon-name: "selection-mode-symbolic";
tooltip-text: _("Toggle Selection Mode");
}
}
[top]
SearchBar search_bar {
search-mode-enabled: bind search_button.active bidirectional;
key-capture-widget: main_toolbar_view;
Adw.Clamp {
maximum-size: 577;
hexpand: true;
SearchEntry search_entry {}
}
}
content: Adw.ToastOverlay toast_overlay {
Overlay main_overlay {
Stack main_stack {
Adw.StatusPage loading_flatpaks {
icon-name: "clock-alt-symbolic";
title: _("Loading Flatpaks");
description: _("This should only take a moment");
}
Box main_box {
orientation: vertical;
ScrolledWindow scrolled_window {
vexpand: true;
Adw.Clamp {
ListBox flatpaks_list_box {
margin-top: 12;
margin-bottom: 12;
margin-start: 12;
margin-end: 12;
hexpand: true;
valign: start;
selection-mode: none;
styles [
"boxed-list"
]
}
}
}
}
Box installing {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label {
label: _("Installing");
styles [
"title-1",
"title"
]
}
Label {
label: _("This could take a while");
styles ["description", "body"]
}
}
Box uninstalling {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label {
label: _("Uninstalling");
styles ["title-1", "title"]
}
Label uninstalling_status {
label: "";
justify: center;
styles ["description", "body"]
}
}
Box snapshotting {
orientation: vertical;
spacing: 10;
margin-top: 40;
margin-bottom: 20;
halign: center;
valign: center;
Spinner {
margin-bottom: 35;
width-request: 30;
height-request: 30;
opacity: 0.5;
spinning: true;
}
Label {
label: _("Creating Snapshots");
styles ["title-1", "title"]
}
Label {
label: _("This could take a while");
styles ["description", "body"]
}
}
Adw.StatusPage no_flatpaks {
icon-name: "error-symbolic";
title: _("No Flatpaks Found");
description: _("Warehouse cannot see the list of installed Flatpaks or the system has no Flatpaks installed");
}
Adw.StatusPage no_matches {
icon-name: "funnel-symbolic";
title: _("No Flatpaks Match Filters");
description: _("No installed Flatpak matches all of the currently applied filters");
[child]
Button reset_filters_button {
label: _("Reset Filters");
halign: center;
styles["pill"]
}
}
Adw.StatusPage no_results {
icon-name: "system-search-symbolic";
title: _("No Results Found");
description: _("Try a different search term");
}
Adw.StatusPage refreshing {
icon-name: "arrow-circular-top-right-symbolic";
title: _("Refreshing List");
description: _("This should only take a moment");
}
}
}
};
[bottom]
ActionBar batch_mode_bar {
revealed: false;
[start]
ToggleButton batch_select_all_button {
label: _("Select All");
}
[end]
Button batch_uninstall_button {
icon-name: "cross-filled-symbolic";
tooltip-text: _("Uninstall Selected Apps");
}
[end]
Button batch_clean_button {
icon-name: "user-trash-symbolic";
tooltip-text: _("Send Selected Apps' Data to the Trash");
}
[end]
MenuButton batch_copy_button {
icon-name: "edit-copy-symbolic";
tooltip-text: _("Open Copy Menu");
menu-model: copy_menu;
}
[end]
Button batch_snapshot_button {
icon-name: "clock-alt-symbolic";
tooltip-text: _("Snapshot Selected Apps' Data");
visible: true;
}
}
}
}
menu primary_menu {
section {
item {
label: _("Manage Leftover Data…");
action: "app.manage-data-folders";
}
/*item {
label: _("_Preferences");
action: "app.preferences";
}*/
item {
label: _("Manage Remotes…");
action: "app.show-remotes-window";
}
}
section {
item {
label: _("Install From File…");
action: "app.install-from-file";
}
item {
label: _("Install From The Web…");
action: "app.open-search-install";
}
}
section {
item {
label: _("Refresh List");
action: "app.refresh-list";
}
item {
label: _("_Keyboard Shortcuts");
action: "win.show-help-overlay";
}
item {
label: _("_About Warehouse");
action: "app.about";
}
}
}
menu copy_menu {
section {
item {
label: _("Copy Names");
action: "win.copy-names";
}
item {
label: _("Copy IDs");
action: "win.copy-ids";
}
item {
label: _("Copy Refs");
action: "win.copy-refs";
}
}
}

View File

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

View File

@@ -1,5 +1,5 @@
project('warehouse',
version: '1.6.5',
version: '2.0.0',
meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ],
)

View File

@@ -1,23 +1,61 @@
data/io.github.flattool.Warehouse.desktop.in
data/io.github.flattool.Warehouse.metainfo.xml.in
data/io.github.flattool.Warehouse.gschema.xml
data/ui/downgrade.blp
data/ui/filter.blp
data/ui/orphans.blp
data/ui/popular_remotes.blp
data/ui/properties.blp
data/ui/remotes.blp
data/ui/search_install.blp
data/ui/snapshots.blp
data/ui/window.blp
src/app_row_widget.py
src/common.py
src/downgrade_window.py
src/filter_window.py
src/gtk/help-overlay.blp
src/main.py
src/orphans_window.py
src/properties_window.py
src/remotes_window.py
src/snapshots_window.py
src/window.py
src/host_info.py
src/package_install_worker.py
src/gtk/app_row.blp
src/gtk/attempt_install_dialog.blp
src/gtk/attempt_install_dialog.py
src/gtk/error_toast.py
src/gtk/help-overlay.blp
src/gtk/installation_chooser.blp
src/gtk/installation_chooser.py
src/gtk/loading_status.blp
src/gtk/sidebar_button.py
src/main_window/window.blp
src/main_window/window.py
src/packages_page/filters_page.blp
src/packages_page/filters_page.py
src/packages_page/packages_page.blp
src/packages_page/packages_page.py
src/packages_page/uninstall_dialog.blp
src/packages_page/uninstall_dialog.py
src/properties_page/properties_page.blp
src/properties_page/properties_page.py
src/change_version_page/change_version_page.blp
src/change_version_page/change_version_page.py
src/change_version_page/change_version_worker.py
src/remotes_page/add_remote_dialog.blp
src/remotes_page/add_remote_dialog.py
src/remotes_page/remote_row.blp
src/remotes_page/remote_row.py
src/remotes_page/remotes_page.blp
src/remotes_page/remotes_page.py
src/user_data_page/data_box.blp
src/user_data_page/data_box.py
src/user_data_page/data_subpage.blp
src/user_data_page/data_subpage.py
src/user_data_page/user_data_page.blp
src/user_data_page/user_data_page.py
src/snapshot_page/new_snapshot_dialog.blp
src/snapshot_page/new_snapshot_dialog.py
src/snapshot_page/snapshot_box.blp
src/snapshot_page/snapshot_box.py
src/snapshot_page/snapshot_page.blp
src/snapshot_page/snapshot_page.py
src/snapshot_page/snapshots_list_page.blp
src/snapshot_page/snapshots_list_page.py
src/snapshot_page/tar_worker.py
src/install_page/file_install_dialog.blp
src/install_page/file_install_dialog.py
src/install_page/install_page.blp
src/install_page/install_page.py
src/install_page/pending_page.blp
src/install_page/pending_page.py
src/install_page/result_row.blp
src/install_page/result_row.py
src/install_page/results_page.blp
src/install_page/results_page.py
src/install_page/select_page.blp
src/install_page/select_page.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,381 +0,0 @@
import os
import pathlib
import subprocess
import re
from gi.repository import Adw, Gdk, Gio, GLib, Gtk
from .properties_window import PropertiesWindow
from .downgrade_window import DowngradeWindow
from .snapshots_window import SnapshotsWindow
from .filter_window import FilterWindow
from .common import myUtils
class AppRow(Adw.ActionRow):
def set_selectable(self, is_selectable):
self.tickbox.set_active(False)
self.tickbox.set_visible(is_selectable)
self.row_menu.set_visible(not is_selectable)
self.set_activatable(is_selectable)
def set_is_visible(self, is_visible):
self.set_visible(is_visible)
self.set_selectable(False)
def info_button_show_or_hide(self):
self.info_button.set_visible(False)
if self.mask_label.get_visible() == True:
self.info_button.set_visible(True)
return
if self.pin_label.get_visible() == True:
self.info_button.set_visible(True)
return
if self.eol_app_label.get_visible() == True:
self.info_button.set_visible(True)
return
if self.eol_runtime_label.get_visible() == True:
self.info_button.set_visible(True)
return
def set_masked(self, is_masked):
self.mask_label.set_visible(is_masked)
self.info_button_show_or_hide()
def __init__(self, parent_window, host_flatpaks, index, **kwargs):
super().__init__(**kwargs)
self.my_utils = myUtils(parent_window)
current_flatpak = host_flatpaks[index]
self.index = index
self.app_name = current_flatpak[0]
self.app_id = current_flatpak[2]
self.app_version = current_flatpak[3]
self.origin_remote = current_flatpak[6]
self.install_type = current_flatpak[7]
self.app_ref = current_flatpak[8]
self.dependent_runtime = current_flatpak[13]
self.set_title(self.app_name)
self.set_subtitle(self.app_id)
self.add_prefix(self.my_utils.find_app_icon(self.app_id))
self.is_runtime = False
if len(current_flatpak[13]) == 0:
self.is_runtime = True
info_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
hexpand=True,
vexpand=True,
spacing=6,
)
# justify=Gtk.Justification.RIGHT
self.mask_label = Gtk.Label(
label=_("Updates Disabled"),
visible=False,
hexpand=True,
wrap=True,
valign=Gtk.Align.CENTER,
tooltip_text=_("{} is masked and will not be updated").format(
self.app_name
),
)
self.mask_label.add_css_class("warning")
self.pin_label = Gtk.Label(
label=_("Auto Removal Disabled"),
visible=False,
hexpand=True,
wrap=True,
valign=Gtk.Align.CENTER,
tooltip_text=_("{} is pinned and will not be auto removed even when it's required by no app").format(
self.app_name
)
)
self.pin_label.add_css_class("warning")
self.eol_app_label = Gtk.Label(
label=_("App EOL"),
visible=False,
hexpand=True,
wrap=True,
valign=Gtk.Align.CENTER,
tooltip_text=_(
"{} has reached its End of Life and will not receive any security updates"
).format(self.app_name),
)
self.eol_app_label.add_css_class("error")
info_box.append(self.eol_app_label)
if "eol" in parent_window.host_flatpaks[index][12]:
# EOL = End Of Life, meaning the app will not be updated
# justify=Gtk.Justification.RIGHT
self.eol_app_label.set_visible(True)
self.eol_runtime_label = Gtk.Label(
label=_("Runtime EOL"),
visible=False,
hexpand=True,
wrap=True,
valign=Gtk.Align.CENTER,
tooltip_text=_(
"{}'s runtime has reached its End of Life and will not receive any security updates"
).format(self.app_name),
)
self.eol_runtime_label.add_css_class("error")
info_box.append(self.eol_runtime_label)
if current_flatpak[13] in parent_window.eol_list:
# EOL = End Of Life, meaning the runtime will not be updated
# justify=Gtk.Justification.RIGHT
self.eol_runtime_label.set_visible(True)
info_pop = Gtk.Popover()
info_pop.set_child(info_box)
self.info_button = Gtk.MenuButton(
visible=False,
valign=Gtk.Align.CENTER,
popover=info_pop,
icon_name="software-update-urgent-symbolic",
)
self.info_button.add_css_class("flat")
info_box.append(self.mask_label)
info_box.append(self.pin_label)
self.add_suffix(self.info_button)
properties_button = Gtk.Button(
icon_name="info-symbolic",
valign=Gtk.Align.CENTER,
tooltip_text=_("View Properties"),
)
properties_button.add_css_class("flat")
properties_button.connect(
"clicked", lambda *_: PropertiesWindow(index, host_flatpaks, parent_window)
)
self.add_suffix(properties_button)
self.tickbox = Gtk.CheckButton(
visible=False, tooltip_text=_("Select")
) # visible=self.in_batch_mode
self.tickbox.add_css_class("selection-mode")
self.tickbox.connect("toggled", parent_window.row_select_handler)
self.add_suffix(self.tickbox)
self.set_activatable_widget(self.tickbox)
self.set_activatable(False)
self.row_menu = Gtk.MenuButton(
icon_name="view-more-symbolic",
valign=Gtk.Align.CENTER,
tooltip_text=_("View More"),
)
self.row_menu.add_css_class("flat")
row_menu_model = Gio.Menu()
copy_menu_model = Gio.Menu()
advanced_menu_model = Gio.Menu()
self.add_suffix(self.row_menu)
self.is_pinned = False
if "user" in self.install_type:
if f"runtime/{self.app_ref}" in parent_window.user_pins:
self.is_pinned = True
if "system" in self.install_type:
if f"runtime/{self.app_ref}" in parent_window.system_pins:
self.is_pinned = True
parent_window.create_action(
("copy-name" + str(index)),
lambda *_, name=self.app_name, toast=_(
"Copied name"
): parent_window.copy_item(name, toast),
)
copy_menu_model.append_item(
Gio.MenuItem.new(_("Copy Name"), f"win.copy-name{index}")
)
parent_window.create_action(
("copy-id" + str(index)),
lambda *_, id=self.app_id, toast=_("Copied ID"): parent_window.copy_item(
id, toast
),
)
copy_menu_model.append_item(
Gio.MenuItem.new(_("Copy ID"), f"win.copy-id{index}")
)
parent_window.create_action(
("copy-ref" + str(index)),
lambda *_, ref=self.app_ref, toast=_("Copied ref"): parent_window.copy_item(
ref, toast
),
)
copy_menu_model.append_item(
Gio.MenuItem.new(_("Copy Ref"), f"win.copy-ref{index}")
)
parent_window.create_action(
("copy-command" + str(index)),
lambda *_, ref=self.app_ref, toast=_(
"Copied launch command"
): parent_window.copy_item(f"flatpak run {ref}", toast),
)
copy_menu_model.append_item(
Gio.MenuItem.new(_("Copy Launch Command"), f"win.copy-command{index}")
)
row_menu_model.append_submenu(_("Copy"), copy_menu_model)
if (
"runtime" not in parent_window.host_flatpaks[index][12]
and self.app_id != "io.github.flattool.Warehouse"
):
parent_window.create_action(
("run" + str(index)),
lambda *_a, ref=self.app_ref, name=self.app_name: parent_window.run_app_thread(
ref, _("Opened {}").format(name)
),
)
run_item = Gio.MenuItem.new(_("Open"), f"win.run{index}")
row_menu_model.append_item(run_item)
if self.app_id != "io.github.flattool.Warehouse":
parent_window.create_action(
("uninstall" + str(index)),
lambda *_: parent_window.uninstall_button_handler(
self, self.app_name, self.app_ref, self.app_id
),
)
uninstall_item = Gio.MenuItem.new(_("Uninstall"), f"win.uninstall{index}")
row_menu_model.append_item(uninstall_item)
data_menu_model = Gio.Menu()
parent_window.create_action(
("open-data" + str(index)),
lambda *_, path=(
parent_window.user_data_path + self.app_id
): parent_window.open_data_folder(path),
)
open_data_item = Gio.MenuItem.new(
_("Open User Data Folder"), f"win.open-data{index}"
)
open_data_item.set_attribute_value(
"hidden-when", GLib.Variant.new_string("action-disabled")
)
data_menu_model.append_item(open_data_item)
parent_window.create_action(
("trash" + str(index)),
lambda *_, name=self.app_name, id=self.app_id, index=index: parent_window.trash_data(
name, id, index
),
)
trash_item = Gio.MenuItem.new(_("Trash User Data"), f"win.trash{index}")
trash_item.set_attribute_value(
"hidden-when", GLib.Variant.new_string("action-disabled")
)
data_menu_model.append_item(trash_item)
row_menu_model.append_section(None, data_menu_model)
if not os.path.exists(parent_window.user_data_path + self.app_id):
parent_window.lookup_action(f"open-data{self.index}").set_enabled(False)
parent_window.lookup_action(f"trash{self.index}").set_enabled(False)
parent_window.create_action(
("mask" + str(index)),
lambda *_, id=self.app_id, type=self.install_type, index=index: parent_window.mask_flatpak(
self
),
)
mask_item = Gio.MenuItem.new(_("Disable Updates"), f"win.mask{index}")
mask_item.set_attribute_value(
"hidden-when", GLib.Variant.new_string("action-disabled")
)
advanced_menu_model.append_item(mask_item)
parent_window.create_action(
("unmask" + str(index)),
lambda *_, id=self.app_id, type=self.install_type, index=index: parent_window.mask_flatpak(
self
),
)
unmask_item = Gio.MenuItem.new(_("Enable Updates"), f"win.unmask{index}")
unmask_item.set_attribute_value(
"hidden-when", GLib.Variant.new_string("action-disabled")
)
advanced_menu_model.append_item(unmask_item)
if "runtime" in parent_window.host_flatpaks[index][12]:
parent_window.create_action(
("pin" + str(index)),
lambda *_, d=self.app_id, type=self.install_type, index=index: parent_window.pin_flatpak(
self
),
)
pin_item = Gio.MenuItem.new(_("Disable Auto Removal"), f"win.pin{index}")
pin_item.set_attribute_value(
"hidden-when", GLib.Variant.new_string("action-disabled")
)
advanced_menu_model.append_item(pin_item)
parent_window.create_action(
("unpin" + str(index)),
lambda *_, d=self.app_id, type=self.install_type, index=index: parent_window.pin_flatpak(
self
),
)
unpin_item = Gio.MenuItem.new(_("Enable Auto Removal"), f"win.unpin{index}")
unpin_item.set_attribute_value(
"hidden-when", GLib.Variant.new_string("action-disabled")
)
advanced_menu_model.append_item(unpin_item)
if "runtime" not in parent_window.host_flatpaks[index][12]:
parent_window.create_action(
("snapshot" + str(index)),
lambda *_, row=current_flatpak: SnapshotsWindow(
parent_window, row
),
)
snapshot_item = Gio.MenuItem.new(
_("Manage Snapshots"), f"win.snapshot{index}"
)
advanced_menu_model.append_item(snapshot_item)
parent_window.create_action(
("downgrade" + str(index)),
lambda *_, row=current_flatpak, index=index: DowngradeWindow(
parent_window, row, index
),
)
downgrade_item = Gio.MenuItem.new(_("Downgrade"), f"win.downgrade{index}")
advanced_menu_model.append_item(downgrade_item)
if (
self.app_id in parent_window.system_mask_list
or self.app_id in parent_window.user_mask_list
):
self.mask_label.set_visible(True)
parent_window.lookup_action(f"mask{index}").set_enabled(False)
else:
parent_window.lookup_action(f"unmask{index}").set_enabled(False)
if self.is_runtime and self.is_pinned:
parent_window.lookup_action(f"pin{index}").set_enabled(False)
elif self.is_runtime:
parent_window.lookup_action(f"unpin{index}").set_enabled(False)
self.pin_label.set_visible(self.is_pinned)
row_menu_model.append_section(None, advanced_menu_model)
self.row_menu.set_menu_model(row_menu_model)
self.info_button_show_or_hide()

View File

@@ -0,0 +1,60 @@
using Gtk 4.0;
using Adw 1;
template $ChangeVersionPage : Adw.NavigationPage {
title: _("Change Versions");
Adw.ToolbarView {
[top]
Adw.HeaderBar {
}
Adw.ToastOverlay toast_overlay {
ScrolledWindow scrolled_window {}
}
[bottom]
ActionBar action_bar {
revealed: false;
[center]
Button apply_button {
sensitive: bind action_bar.revealed;
halign: center;
margin-top: 3;
margin-bottom: 3;
Adw.ButtonContent {
label: _("Change Version");
icon-name: "double-ended-arrows-vertical-symbolic";
}
styles ["suggested-action", "pill"]
}
}
}
}
Adw.Clamp versions_clamp {
Box {
margin-start: 12;
margin-end: 12;
margin-top: 12;
margin-bottom: 12;
spacing: 12;
orientation: vertical;
halign: fill;
hexpand: true;
CheckButton root_group_check_button {
visible: false;
active: true;
}
Adw.PreferencesGroup mask_group {
Adw.SwitchRow mask_row {
title: _("Disable Updates");
active: true;
}
}
Adw.PreferencesGroup versions_group {
title: _("Select a Release");
description: _("This will uninstall the current release and install the chosen one instead. Note that downgrading can cause issues.");
}
}
}

View File

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

View File

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

View File

@@ -1,535 +0,0 @@
from gi.repository import GLib, Gtk, Adw, Gio # , Gdk
import os
import subprocess
import pathlib
import time
class myUtils:
def __init__(self, window, **kwargs):
self.parent_window = window
self.host_home = str(pathlib.Path.home())
self.user_data_path = self.host_home + "/.var/app/"
self.install_success = True
self.uninstall_success = True
self.new_env = dict(os.environ)
self.new_env["LC_ALL"] = "C"
self.is_dialog_open = False
def trash_folder(self, path):
if not os.path.exists(path):
print("error in common.trashFolder: path does not exists. path =", path)
return 1
try:
subprocess.run(
["gio", "trash", path],
capture_output=False,
check=True,
env=self.new_env,
)
return 0
except subprocess.CalledProcessError as e:
print("error in common.trashFolder: CalledProcessError:", e)
return 2
def get_size_with_format(self, path):
return self.get_size_format(self.get_dir_size(path))
def get_size_format(self, b):
factor = 1000
suffix = "B"
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if b < factor:
return f"{b:.1f}{unit}{suffix}"
b /= factor
return f"{b:.1f}{suffix}"
def get_dir_size(self, directory):
"""Returns the `directory` size in bytes."""
total = 0
try:
# print("[+] Getting the size of", directory)
for entry in os.scandir(directory):
if entry.is_symlink():
continue # Skip symlinks
if entry.is_file():
# if it's a file, use stat() function
total += entry.stat().st_size
elif entry.is_dir():
# if it's a directory, recursively call this function
try:
total += self.get_dir_size(entry.path)
except FileNotFoundError:
pass
except NotADirectoryError:
# if `directory` isn't a directory, get the file size then
return os.path.getsize(directory)
except PermissionError:
# if for whatever reason we can't open the folder, return 0
return 0
if total == 0:
return 0
# Adding 4000 seems to make it more accurate to whatever data we can't scan from within the sandbox
return total + 4000
def find_app_icon(self, app_id):
icon_theme = Gtk.IconTheme.new()
icon_theme.add_search_path("/var/lib/flatpak/exports/share/icons/")
icon_theme.add_search_path(
self.host_home + "/.local/share/flatpak/exports/share/icons"
)
try:
icon_path = (
icon_theme.lookup_icon(
app_id, None, 512, 1, self.parent_window.get_direction(), 0
)
.get_file()
.get_path()
)
except GLib.GError:
icon_path = None
if icon_path:
image = Gtk.Image.new_from_file(icon_path)
image.set_icon_size(Gtk.IconSize.LARGE)
image.add_css_class("icon-dropshadow")
else:
image = Gtk.Image.new_from_icon_name("application-x-executable-symbolic")
image.set_icon_size(Gtk.IconSize.LARGE)
return image
def get_host_updates(self):
list = []
output = subprocess.run(
["flatpak-spawn", "--host", "flatpak", "update"],
capture_output=True,
text=True,
env=self.new_env,
).stdout
lines = output.strip().split("\n")
columns = lines[0].split("\t")
data = [columns]
for line in lines[1:]:
row = line.split("\t")
data.append(row)
for i in range(len(data)):
if data[i][0].find(".") == 2:
list.append(data[i][2])
return list
def get_host_system_pins(self):
output = subprocess.run(
["flatpak-spawn", "--host", "flatpak", "pin"],
capture_output=True,
text=True,
env=self.new_env,
).stdout
data = output.strip().split("\n")
for i in range(len(data)):
data[i] = data[i].strip()
return data
def get_host_user_pins(self):
output = subprocess.run(
["flatpak-spawn", "--host", "flatpak", "pin", "--user"],
capture_output=True,
text=True,
env=self.new_env,
).stdout
data = output.strip().split("\n")
for i in range(len(data)):
data[i] = data[i].strip()
return data
def get_host_remotes(self):
output = subprocess.run(
[
"flatpak-spawn",
"--host",
"flatpak",
"remotes",
"--columns=all",
"--show-disabled",
],
capture_output=True,
text=True,
env=self.new_env,
).stdout
lines = output.strip().split("\n")
columns = lines[0].split("\t")
data = [columns]
for line in lines[1:]:
row = line.split("\t")
data.append(row)
return data
def get_host_flatpaks(self):
output = subprocess.run(
["flatpak-spawn", "--host", "flatpak", "list",
"--columns=name,application,version,branch,arch,origin,installation,ref,active,latest,size,options,runtime"],
capture_output=True,
text=True,
env=self.new_env,
).stdout
lines = output.strip().split("\n")
columns = lines[0].split("\t")
data = []
for line in lines:
row = line.split("\t")
row.insert(1, " ")
if len(row) < 14:
row.append("")
data.append(row)
# output = subprocess.run(
# ["flatpak-spawn", "--host", "flatpak", "list", "--columns=runtime"],
# capture_output=True,
# text=True,
# env=self.new_env,
# ).stdout
# lines = output.split("\n")
# for i in range(len(data)):
# data[i].append(lines[i])
sorted_array = sorted(data, key=lambda item: item[0].lower())
return sorted_array
def get_flatpak_info(self, ref, install_type):
output = subprocess.run(
[
"flatpak-spawn", "--host", "sh", "-c",
f"flatpak info {ref} --{install_type}"
],
capture_output=True,
text=True
).stdout
lines = output.strip().split("\n")
columns = lines[0].split("\t")
data = []
for line in lines:
row = line.split(": ", 1)
for i in range(len(row)):
row[i] = row[i].strip()
data.append(row)
info = {}
maybe_name = data[0][0]
if not "ID" in maybe_name:
info["name"] = data[0][0]
else:
info["name"] = ref.split("/")[0].split(".")[-1]
for i in range(2, len(data)):
if data[i][0] == '':
continue
info[data[i][0]] = data[i][1]
return info
def get_dependent_runtimes(self):
paks = self.get_host_flatpaks()
dependent_runtimes = []
for i in range(len(paks)):
current = paks[i]
try:
if current[13] not in dependent_runtimes and current[13] != "":
dependent_runtimes.append(current[13])
except:
print("Could not get dependent runtime")
return sorted(dependent_runtimes)
def get_host_masks(self, user_or_system):
output = subprocess.run(
["flatpak-spawn", "--host", "flatpak", "mask", f"--{user_or_system}"],
capture_output=True,
text=True,
env=self.new_env,
).stdout
lines = output.strip().split("\n")
for i in range(len(lines)):
lines[i] = lines[i].strip()
return lines
def mask_flatpak(self, app_id, user_or_system, remove=False):
command = [
"flatpak-spawn",
"--host",
"flatpak",
"mask",
f"--{user_or_system}",
app_id,
]
if remove:
command.append("--remove")
response = ""
try:
response = subprocess.run(
command, capture_output=True, text=True, env=self.new_env
)
except subprocess.CalledProcessError as e:
print(f"Error setting mask for {app_id}:\n", e)
return 1
if len(response.stderr) > 0:
return 1
return 0
def downgrade_flatpak(self, id, ref, commit, install_type="system", mask=False, mask_list=None):
unmask_cmd = f"flatpak mask --remove --{install_type} {id} && "
update_cmd = f"flatpak update {ref} --commit={commit} --{install_type} -y"
to_run_cmd = ""
if id in mask_list:
to_run_cmd += unmask_cmd
to_run_cmd += update_cmd
if mask:
to_run_cmd += f" && flatpak mask --{install_type} {id}"
command = [
"flatpak-spawn",
"--host", "pkexec",
"sh", "-c",
to_run_cmd,
]
if install_type == "user":
command.remove("pkexec")
try:
subprocess.run(
command, capture_output=True, text=True, env=self.new_env, check=True
)
except subprocess.CalledProcessError as e:
print("Error in common.downgrade_flatpak:", e.stderr)
return 1
return 0
def uninstall_flatpak(
self, ref_arr, type_arr, should_trash, progress_bar=None, status_label=None
):
self.uninstall_success = True
print(ref_arr)
to_uninstall = []
for i in range(len(ref_arr)):
to_uninstall.append([ref_arr[i], type_arr[i]])
apps = []
fails = []
for i in range(len(to_uninstall)):
ref = to_uninstall[i][0]
id = to_uninstall[i][0].split("/")[0]
app_type = to_uninstall[i][1]
apps.append([ref, id, app_type])
# apps array guide: [app_ref, app_id, user_or_system_install]
for i in range(len(apps)):
command = [
"flatpak-spawn",
"--host",
"flatpak",
"remove",
"-y",
f"--{apps[i][2]}",
apps[i][0],
]
try:
print(apps)
if status_label:
GLib.idle_add(
status_label.set_label,
_("Working on {}\n{} out of {}").format(
apps[i][0], i + 1, len(apps)
),
)
subprocess.run(
command, capture_output=False, check=True, env=self.new_env
)
if progress_bar:
GLib.idle_add(progress_bar.set_visible, True)
GLib.idle_add(progress_bar.set_fraction, (i + 1.0) / len(ref_arr))
except subprocess.CalledProcessError:
fails.append(apps[i])
if len(fails) > 0: # Run this only if there is 1 or more non uninstalled apps
pk_command = [
"flatpak-spawn",
"--host",
"pkexec",
"flatpak",
"remove",
"-y",
"--system",
]
print("second uninstall process")
for i in range(len(fails)):
if fails[i][2] == "user":
self.uninstall_success = False
continue # Skip if app is a user install app
pk_command.append(fails[i][0])
try:
print(pk_command)
if progress_bar:
GLib.idle_add(progress_bar.set_visible, True)
GLib.idle_add(progress_bar.set_fraction, 0.9)
subprocess.run(
pk_command, capture_output=False, check=True, env=self.new_env
)
except subprocess.CalledProcessError:
self.uninstall_success = False
if should_trash:
host_paks = self.get_host_flatpaks()
host_refs = []
for i in range(len(host_paks)):
host_refs.append(host_paks[i][8])
for i in range(len(apps)):
if apps[i][0] in host_refs:
print(f"{apps[i][1]} is still installed")
else:
self.trash_folder(f"{self.user_data_path}{apps[i][1]}")
if progress_bar:
GLib.idle_add(progress_bar.set_visible, False)
GLib.idle_add(progress_bar.set_fraction, 0.0)
def install_flatpak(
self, app_arr, remote, user_or_system, progress_bar=None, status_label=None
):
self.install_success = True
fails = []
for i in range(len(app_arr)):
command = ["flatpak-spawn", "--host", "flatpak", "install"]
if remote != None:
command.append(remote)
command.append(f"--{user_or_system}")
command.append("-y")
command.append(app_arr[i])
try:
if status_label:
GLib.idle_add(
status_label.set_label,
_("Working on {}\n{} out of {}").format(
app_arr[i], i + 1, len(app_arr)
),
)
subprocess.run(
command, capture_output=False, check=True, env=self.new_env
)
if progress_bar:
GLib.idle_add(progress_bar.set_visible, True)
GLib.idle_add(progress_bar.set_fraction, (i + 1.0) / len(app_arr))
except subprocess.CalledProcessError:
fails.append(app_arr[i])
if (len(fails) > 0) and (user_or_system == "system"):
pk_command = [
"flatpak-spawn",
"--host",
"pkexec",
"flatpak",
"install",
remote,
f"--{user_or_system}",
"-y",
]
for i in range(len(fails)):
pk_command.append(fails[i])
try:
if progress_bar:
GLib.idle_add(progress_bar.set_visible, True)
GLib.idle_add(progress_bar.set_fraction, 0.9)
subprocess.run(
pk_command, capture_output=False, check=True, env=self.new_env
)
except subprocess.CalledProcessError:
self.install_success = False
if (len(fails) > 0) and (user_or_system == "user"):
self.install_success = False
if progress_bar:
GLib.idle_add(progress_bar.set_visible, False)
GLib.idle_add(progress_bar.set_fraction, 0.0)
def run_app(self, ref):
self.run_app_error = False
self.run_app_error_message = ""
try:
subprocess.run(
["flatpak-spawn", "--host", "flatpak", "run", ref],
check=True,
env=self.new_env,
start_new_session=True,
capture_output=True,
)
except subprocess.CalledProcessError as e:
self.run_app_error_message = e.stderr.decode()
self.run_app_error = True
def get_install_type(self, type_arr):
if "disabled" in type_arr:
return "disabled"
if "user" in type_arr:
return "user"
if "system" in type_arr:
return "system"
def snapshot_apps(
self,
epoch,
app_snapshot_path_arr,
app_version_arr,
app_user_data_arr,
progress_bar=None,
):
if not (
len(app_snapshot_path_arr) == len(app_version_arr) == len(app_user_data_arr)
):
print(
"error in common.snapshotApp: the lengths of app_snapshot_path_arr, app_version_arr, and app_user_data_arr do not match."
)
return 1
fails = []
for i in range(len(app_snapshot_path_arr)):
snapshot_path = app_snapshot_path_arr[i]
version = app_version_arr[i]
user_data = app_user_data_arr[i]
command = [
"tar",
"cafv",
f"{snapshot_path}{epoch}_{version}.tar.zst",
"-C",
f"{user_data}",
".",
]
try:
if not os.path.exists(snapshot_path):
file = Gio.File.new_for_path(snapshot_path)
file.make_directory()
subprocess.run(command, check=True, env=self.new_env)
if progress_bar:
GLib.idle_add(progress_bar.set_visible, True)
GLib.idle_add(
progress_bar.set_fraction,
(i + 1.0) / len(app_snapshot_path_arr),
)
except subprocess.CalledProcessError as e:
print("error in common.snapshotApp:", e)
fails.append(user_data)
if (
int(time.time()) == epoch
): # Wait 1s if the snapshot is made too quickly, to prevent overriding a snapshot file
subprocess.run(["sleep", "1s"])
if progress_bar:
GLib.idle_add(progress_bar.set_visible, False)
GLib.idle_add(progress_bar.set_fraction, 0.0)
if len(fails) > 0:
print("These paths could not be archived:")
for i in range(fails):
print(fails[i])
print("")
return 1
else:
return 0

View File

@@ -1,168 +0,0 @@
from gi.repository import Gtk, Adw, GLib, Gdk, Gio
from .common import myUtils
import subprocess
import os
import pathlib
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/../data/ui/downgrade.ui")
class DowngradeWindow(Adw.Dialog):
__gtype_name__ = "DowngradeWindow"
new_env = dict(os.environ)
new_env["LC_ALL"] = "C"
apply_button = Gtk.Template.Child()
versions_group = Gtk.Template.Child()
toast_overlay = Gtk.Template.Child()
mask_row = Gtk.Template.Child()
main_toolbar_view = Gtk.Template.Child()
loading = Gtk.Template.Child()
loading_label = Gtk.Template.Child()
main_stack = Gtk.Template.Child()
outerbox = Gtk.Template.Child()
action_bar = Gtk.Template.Child()
def selection_handler(self, button, index):
self.action_bar.set_revealed(True)
if button.get_active():
self.commit_to_use = self.versions[index][0]
def get_commits(self):
output = subprocess.run(
[
"flatpak-spawn", "--host", "sh", "-c",
f"LC_ALL=C flatpak remote-info --log {self.remote} {self.app_ref} --{self.install_type}"
],
capture_output=True,
text=True
).stdout
lines = output.strip().split("\n")
columns = lines[0].split("\t")
data = [columns]
for line in lines[1:]:
row = line.split("\t")
data.append(row[0].strip())
commits = []
changes = []
dates = []
for i in range(len(data)):
line = data[i]
if "Commit:" in line:
commits.append(line.replace("Commit: ", ""))
if "Subject:" in line:
changes.append(line.replace("Subject: ", ""))
if "Date:" in line:
dates.append(line.replace("Date: ", ""))
for i in range(len(commits)):
self.versions.append([commits[i], changes[i], dates[i]])
def commits_callback(self):
group_button = Gtk.CheckButton(visible=False)
self.versions_group.add(group_button)
for i in range(len(self.versions)):
version = self.versions[i]
date_time = version[2].split(" ")
date = date_time[0].split("-")
offset = date_time[2][:3] + ":" + date_time[2][3:]
time = date_time[1].split(":")
display_time = GLib.DateTime.new(
GLib.TimeZone.new(offset),
int(date[0]),
int(date[1]),
int(date[2]),
int(time[0]),
int(time[1]),
int(time[2]),
)
display_time = display_time.format("%x %X")
change = version[1].split("(")
row = Adw.ActionRow(
title=GLib.markup_escape_text(change[0]), subtitle=str(display_time)
)
row.set_tooltip_text(_("Commit Hash: {}").format(version[0]))
select = Gtk.CheckButton()
select.connect("toggled", self.selection_handler, i)
select.set_group(group_button)
version.append(select)
row.set_activatable_widget(select)
row.add_prefix(select)
self.versions_group.add(row)
self.main_stack.set_visible_child(self.outerbox)
self.apply_button.set_visible(True)
self.mask_row.grab_focus() # Don't know why, but I need this in order for escape to close the window
def generate_list(self):
task = Gio.Task.new(None, None, lambda *_: self.commits_callback())
task.run_in_thread(lambda *_: self.get_commits())
def downgrade_callack(self):
self.set_can_close(True)
if self.response != 0:
self.parent_window.toast_overlay.add_toast(
Adw.Toast.new(_("Could not downgrade {}").format(self.app_name))
)
self.apply_button.set_sensitive(True)
self.parent_window.refresh_list_of_flatpaks(self)
self.close()
def downgrade_thread(self):
mask_list = None
if self.install_type == "system":
mask_list = self.parent_window.system_mask_list
if self.install_type == "user":
mask_list = self.parent_window.user_mask_list
self.response = self.my_utils.downgrade_flatpak(
self.app_id, self.app_ref, self.commit_to_use, self.install_type, self.mask_row.get_active(), mask_list
)
def on_apply(self):
self.loading_label.set_label(_("Downgrading…"))
self.set_can_close(False)
self.main_stack.set_visible_child(self.loading)
self.apply_button.set_visible(False)
task = Gio.Task.new(None, None, lambda *_: self.downgrade_callack())
task.run_in_thread(lambda *_: self.downgrade_thread())
def __init__(self, parent_window, flatpak_row, index, **kwargs):
super().__init__(**kwargs)
# Create Variables
self.my_utils = myUtils(self)
self.app_name = flatpak_row[0]
self.app_id = flatpak_row[2]
self.remote = flatpak_row[6]
self.install_type = flatpak_row[7]
self.app_ref = flatpak_row[8]
self.versions = []
self.commit_to_use = ""
self.parent_window = parent_window
self.flatpak_row = flatpak_row
self.response = 0
self.window_title = _("Downgrade {}").format(self.app_name)
self.index = index
# Connections
self.apply_button.connect("clicked", lambda *_: self.on_apply())
# Apply
self.mask_row.set_subtitle(
_("Ensure that {} will never be updated to a newer version").format(
self.app_name
)
)
self.set_title(self.window_title)
self.generate_list()
self.present(parent_window)

View File

@@ -1,226 +0,0 @@
from gi.repository import Gtk, Adw, GLib, Gdk, Gio
from .common import myUtils
import subprocess
import os
import pathlib
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/../data/ui/filter.ui")
class FilterWindow(Adw.Dialog):
__gtype_name__ = "FilterWindow"
show_apps_switch = Gtk.Template.Child()
show_runtimes_switch = Gtk.Template.Child()
remotes_expander = Gtk.Template.Child()
runtimes_expander = Gtk.Template.Child()
reset_button = Gtk.Template.Child()
is_open = False
def gsettings_bool_set(self, key, value):
self.settings.set_boolean(key, value)
self.check_is_resetable()
self.main_window.apply_filter()
def check_is_resetable(self):
if not self.show_apps_switch.get_active():
self.reset_button.set_sensitive(True)
return
if self.show_runtimes_switch.get_active():
self.reset_button.set_sensitive(True)
return
if self.total_remotes_selected != 0:
self.reset_button.set_sensitive(True)
return
if self.total_runtimes_selected != 0:
self.reset_button.set_sensitive(True)
return
self.reset_button.set_sensitive(False)
def row_subtitle_updater(self):
if self.total_runtimes_selected > 0:
self.runtimes_expander.set_subtitle(
_("{} selected").format(self.total_runtimes_selected)
)
else:
self.runtimes_expander.set_subtitle("")
if self.total_remotes_selected > 0:
self.remotes_expander.set_subtitle(
_("{} selected").format(self.total_remotes_selected)
)
else:
self.remotes_expander.set_subtitle("")
def reset_filter_gsettings(self):
self.show_apps_switch.set_active(True)
self.show_runtimes_switch.set_active(False)
for button in self.remote_checkboxes:
button.set_active(False)
for button in self.runtime_checkboxes:
button.set_active(False)
for key in self.settings.list_keys():
self.settings.reset(key)
self.total_remotes_selected = 0
self.total_runtimes_selected = 0
self.row_subtitle_updater()
self.reset_button.set_sensitive(False)
def runtime_handler(self, button, runtime):
if button.get_active():
self.total_runtimes_selected += 1
self.runtimes_string = self.runtimes_string.replace("all", "")
self.runtimes_string += f"{runtime},"
else:
self.total_runtimes_selected -= 1
self.runtimes_string = self.runtimes_string.replace(f"{runtime},", "")
if len(self.runtimes_string) < 1:
self.runtimes_string += "all"
self.settings.set_string("runtimes-list", self.runtimes_string)
self.check_is_resetable()
self.row_subtitle_updater()
self.main_window.apply_filter()
def remote_handler(self, button, remote, install_type):
if button.get_active():
self.total_remotes_selected += 1
self.remotes_string = self.remotes_string.replace("all", "")
self.remotes_string += f"{remote}<>{install_type};"
else:
self.total_remotes_selected -= 1
self.remotes_string = self.remotes_string.replace(
f"{remote}<>{install_type};", ""
)
if len(self.remotes_string) < 1:
self.remotes_string += "all"
self.settings.set_string("remotes-list", self.remotes_string)
self.check_is_resetable()
self.row_subtitle_updater()
self.main_window.apply_filter()
def generate_remotes(self):
if (
len(self.host_remotes) < 2
): # Don't give the ability to filter by remotes if there is only 1
self.remotes_expander.set_visible(False)
total = 0
for i in range(len(self.host_remotes)):
try:
name = self.host_remotes[i][0]
title = self.host_remotes[i][1]
url = self.host_remotes[i][2]
install_type = self.my_utils.get_install_type(self.host_remotes[i][7])
remote_row = Adw.ActionRow(title=title)
if "disabled" in install_type:
continue
total += 1
if title == "-":
remote_row.set_title(name)
self.remotes_expander.add_row(remote_row)
label = Gtk.Label(label=("{} wide").format(install_type))
label.add_css_class("subtitle")
remote_check = Gtk.CheckButton()
if name in self.remotes_string:
remote_check.set_active(True)
self.total_remotes_selected += 1
remote_check.connect(
"toggled",
lambda button=remote_check, remote=name, install_type=install_type: self.remote_handler(
button, remote, install_type
),
)
self.remote_checkboxes.append(remote_check)
if "user" in install_type:
remote_row.set_subtitle(_("User wide"))
elif "system" in install_type:
remote_row.set_subtitle(_("System wide"))
else:
remote_row.set_subtitle(_("Unknown install type"))
remote_row.add_suffix(remote_check)
remote_row.set_activatable_widget(remote_check)
except Exception as e:
print(
"error at filter_window.generate_remotes: Could not make remote row. error",
e,
)
self.row_subtitle_updater()
if total < 2:
self.remotes_expander.set_visible(False)
def generate_runtimes(self):
if (
len(self.dependent_runtimes) < 2
): # Don't give the ability to filter by runtimes if there is only 1
self.runtimes_expander.set_visible(False)
for current in self.dependent_runtimes:
runtime_row = Adw.ActionRow(title=current)
runtime_check = Gtk.CheckButton()
if current in self.runtimes_string:
runtime_check.set_active(True)
self.total_runtimes_selected += 1
runtime_check.connect(
"toggled",
lambda button=runtime_check, runtime=current: self.runtime_handler(
button, runtime
),
)
self.runtime_checkboxes.append(runtime_check)
runtime_row.add_suffix(runtime_check)
runtime_row.set_activatable_widget(runtime_check)
self.runtimes_expander.add_row(runtime_row)
self.row_subtitle_updater()
def __init__(self, main_window, **kwargs):
super().__init__(**kwargs)
# Create Variables
self.main_window = main_window
self.my_utils = myUtils(self)
self.host_remotes = self.my_utils.get_host_remotes()
self.dependent_runtimes = self.my_utils.get_dependent_runtimes()
self.settings = Gio.Settings.new("io.github.flattool.Warehouse.filter")
self.remotes_string = self.settings.get_string("remotes-list")
self.runtimes_string = self.settings.get_string("runtimes-list")
self.remote_checkboxes = []
self.runtime_checkboxes = []
self.total_remotes_selected = 0
self.total_runtimes_selected = 0
self.show_apps_switch.set_active(self.settings.get_boolean("show-apps"))
self.show_runtimes_switch.set_active(self.settings.get_boolean("show-runtimes"))
# Connections
self.show_apps_switch.connect(
"state-set",
lambda button, state: self.gsettings_bool_set("show-apps", state),
)
self.show_runtimes_switch.connect(
"state-set",
lambda button, state: self.gsettings_bool_set("show-runtimes", state),
)
self.reset_button.connect("clicked", lambda *_: self.reset_filter_gsettings())
# Calls
if self.host_remotes[0][0] == "":
self.remotes_expander.set_visible(False)
else:
self.generate_remotes()
if self.dependent_runtimes == []:
self.runtimes_expander.set_visible(False)
else:
self.generate_runtimes()
self.check_is_resetable()
def set_is_open_false(*args):
self.__class__.is_open = False
self.connect("closed", set_is_open_false)
if self.__class__.is_open:
return
else:
self.present(main_window)
self.__class__.is_open = True

43
src/gtk/app_row.blp Normal file
View File

@@ -0,0 +1,43 @@
using Gtk 4.0;
using Adw 1;
template $AppRow : Adw.ActionRow {
activatable: true;
[prefix]
Image image {
icon-size: large;
icon-name: "application-x-executable-symbolic";
}
[suffix]
Image eol_package_package_status_icon {
icon-name: "error-symbolic";
tooltip-text: _("This package is End Of Life, and will not receive any security updates");
visible: false;
styles["error"]
}
[suffix]
Image eol_runtime_status_icon {
icon-name: "error-symbolic";
tooltip-text: _("This app's runtime is End Of Life, and will not receive any security updates");
visible: false;
styles["error"]
}
[suffix]
Image pinned_status_icon {
icon-name: "pin-symbolic";
tooltip-text: _("This runtime will never be automatically removed");
visible: false;
}
[suffix]
Image masked_status_icon {
icon-name: "software-update-urgent-symbolic";
tooltip-text: _("Updates are disabled for this package");
visible: false;
}
[suffix]
CheckButton check_button {
margin-start: 6;
styles["selection-mode"]
visible: false;
}
}

45
src/gtk/app_row.py Normal file
View File

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

View File

@@ -0,0 +1,15 @@
using Gtk 4.0;
using Adw 1;
template $AttemptInstallDialog : Adw.AlertDialog {
heading: _("Attempt an Install?");
body: _("Warehouse will try to install the matching packages.");
responses [
cancel: _("Cancel"),
continue: _("Install") suggested,
]
Adw.PreferencesGroup preferences_group {
title: _("Choose a Remote");
description: _("Select a remote to attempt to install from");
}
}

View File

@@ -0,0 +1,76 @@
from gi.repository import Adw, Gtk
from .host_info import HostInfo
from .error_toast import ErrorToast
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/gtk/attempt_install_dialog.ui")
class AttemptInstallDialog(Adw.AlertDialog):
__gtype_name__ = "AttemptInstallDialog"
gtc = Gtk.Template.Child
preferences_group = gtc()
def generate_list(self):
for installation, remotes in HostInfo.remotes.items():
for remote in remotes:
if remote.disabled:
continue
row = Adw.ActionRow(title=remote.title, subtitle=_("Installation: {}").format(installation))
row.remote_name = remote.name
row.remote_installation = installation
button = Gtk.CheckButton()
row.add_prefix(button)
row.check_button = button
row.set_activatable_widget(button)
self.rows.append(row)
self.preferences_group.add(row)
if len(self.rows) > 1:
button.set_group(self.rows[0].check_button)
else:
button.activate()
def on_response(self, dialog, response):
if response != "continue":
if not self.callback is None:
self.callback(False)
return
active_row = None
for row in self.rows:
if row.check_button.get_active():
active_row = row
break
if not active_row is None:
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
HostInfo.main_window.activate_row(HostInfo.main_window.install_row)
self.callback(True)
install_page.install_packages([{
"remote": row.remote_name,
"installation": row.remote_installation,
"package_names": self.package_names,
"extra_flags": [],
}])
elif not self.callback is None:
self.callback(False)
def __init__(self, package_names, callback=None, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
self.rows = []
self.package_names = package_names
self.callback = callback
# Apply
self.generate_list()
if len(self.rows) == 1:
self.set_extra_child(None)
elif len(self.rows) < 1:
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Can't find matching packages"), _("Your system has no remotes added")).toast)
self.present(HostInfo.main_window)
# Connections
self.connect("response", self.on_response)

29
src/gtk/error_toast.py Normal file
View File

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

View File

@@ -1,70 +1,123 @@
using Gtk 4.0;
ShortcutsWindow help_overlay {
modal: true;
ShortcutsSection {
section-name: "shortcuts";
// max-height: 8;
ShortcutsGroup {
title: _("App Management");
ShortcutsShortcut {
title: _("Search");
action-name: "app.search";
}
ShortcutsShortcut {
title: _("Set Filters");
action-name: "app.set-filter";
}
ShortcutsShortcut {
title: _("Refresh");
action-name: "app.refresh-list";
}
ShortcutsShortcut {
title: _("Toggle Selection Mode");
action-name: "app.toggle-batch-mode";
}
}
ShortcutsGroup {
title: _("More Functions");
ShortcutsShortcut {
title: _("Manage Leftover Data");
action-name: "app.manage-data-folders";
}
ShortcutsShortcut {
title: _("Manage Remotes");
action-name: "app.show-remotes-window";
}
ShortcutsShortcut {
title: _("Install From File");
action-name: "app.install-from-file";
}
}
ShortcutsGroup {
title: _("General");
ShortcutsShortcut {
title: _("Open Menu");
action-name: "app.open-menu";
}
ShortcutsShortcut {
title: _("Show Shortcuts");
action-name: "win.show-help-overlay";
}
ShortcutsShortcut {
title: _("Quit");
action-name: "app.quit";
}
}
}
modal: true;
ShortcutsSection {
section-name: "shortcuts";
max-height: 17;
ShortcutsGroup {
title: _("General");
ShortcutsShortcut {
title: _("Refresh");
action-name: "app.refresh";
}
ShortcutsShortcut {
title: _("Open Files");
action-name: "app.open-files";
}
ShortcutsShortcut {
title: _("Open Menu");
action-name: "app.open-menu";
}
ShortcutsShortcut {
title: _("Show Shortcuts");
action-name: "win.show-help-overlay";
}
ShortcutsShortcut {
title: _("Quit");
action-name: "app.quit";
}
}
ShortcutsGroup {
title: _("Navigation");
ShortcutsShortcut {
title: _("Show Packages Page");
action-name: "app.show-packages-page";
}
ShortcutsShortcut {
title: _("Show Remotes Page");
action-name: "app.show-remotes-page";
}
ShortcutsShortcut {
title: _("Show User Data Page");
action-name: "app.show-user-data-page";
}
ShortcutsShortcut {
title: _("Show Snapshots Page");
action-name: "app.show-snapshots-page";
}
ShortcutsShortcut {
title: _("Show Install Page");
action-name: "app.show-install-page";
}
}
ShortcutsGroup {
title: _("Packages Page");
ShortcutsShortcut {
title: _("Search Mode");
action-name: "app.search-mode";
}
ShortcutsShortcut {
title: _("Edit Filters");
action-name: "app.filter";
}
ShortcutsShortcut {
title: _("Selection Mode");
action-name: "app.toggle-select-mode";
}
}
ShortcutsGroup {
title: _("Remotes Page");
ShortcutsShortcut {
title: _("Search Mode");
action-name: "app.search-mode";
}
ShortcutsShortcut {
title: _("Show or Hide Disabled Remotes");
action-name: "app.filter";
}
ShortcutsShortcut {
title: _("New Remote");
action-name: "app.new";
}
}
ShortcutsGroup {
title: _("User Data Page");
ShortcutsShortcut {
title: _("Search Mode");
action-name: "app.search-mode";
}
ShortcutsShortcut {
title: _("Edit Sorting Modes");
action-name: "app.filter";
}
ShortcutsShortcut {
title: _("Selection Mode");
action-name: "app.toggle-select-mode";
}
ShortcutsShortcut {
title: _("Show Active Data");
action-name: "app.active-data-view";
}
ShortcutsShortcut {
title: _("Show Leftover Data");
action-name: "app.leftover-data-view";
}
}
ShortcutsGroup {
title: _("Snapshots Page");
ShortcutsShortcut {
title: _("Search Mode");
action-name: "app.search-mode";
}
ShortcutsShortcut {
title: _("Selection Mode");
action-name: "app.toggle-select-mode";
}
ShortcutsShortcut {
title: _("New Snapshots");
action-name: "app.new";
}
}
}
}

View File

@@ -0,0 +1,40 @@
using Gtk 4.0;
using Adw 1;
template $InstallationChooser : Adw.PreferencesGroup {
title: _("Choose Installation");
Adw.ActionRow user_row {
[prefix]
CheckButton user_check {
active: true;
}
title: _("User");
activatable-widget: user_check;
}
Adw.ActionRow system_row {
[prefix]
CheckButton system_check {
group: user_check;
}
title: _("System");
activatable-widget: system_check;
}
Adw.ActionRow single_row {
visible: false;
[prefix]
CheckButton single_check {
group: user_check;
}
subtitle: _("Custom installation");
activatable-widget: single_check;
}
Adw.ComboRow choice_row {
visible: false;
[prefix]
CheckButton choice_check {
group: user_check;
}
title: _("Other Installation");
subtitle: _("Choose a custom installation");
}
}

View File

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

View File

@@ -0,0 +1,57 @@
using Gtk 4.0;
using Adw 1;
template $LoadingStatus : ScrolledWindow {
Box {
orientation: vertical;
valign: center;
halign: fill;
spacing: 12;
margin-start: 12;
margin-end: 12;
margin-top: 12;
margin-bottom: 12;
Adw.Spinner {
height-request: 30;
margin-bottom: 12;
opacity: 0.5;
}
Label title_label {
label: "No Title Set";
wrap: true;
justify: center;
styles ["title-1"]
}
Label description_label {
label: "No Description Set";
wrap: true;
justify: center;
styles ["description", "body"]
}
Adw.Clamp progress_clamp {
margin-start: 24;
margin-end: 24;
margin-top: 12;
margin-bottom: 12;
maximum-size: 400;
Box {
halign: fill;
hexpand: true;
spacing: 12;
ProgressBar progress_bar {
halign: fill;
hexpand: true;
valign: center;
}
Label progress_label {
valign: center;
}
}
}
Button button {
label: _("Cancel");
styles ["pill"]
halign: center;
}
}
}

25
src/gtk/loading_status.py Normal file
View File

@@ -0,0 +1,25 @@
from gi.repository import Gtk, GLib
from .host_info import HostInfo
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/gtk/loading_status.ui")
class LoadingStatus(Gtk.ScrolledWindow):
__gtype_name__ = 'LoadingStatus'
gtc = Gtk.Template.Child
title_label = gtc()
description_label = gtc()
progress_clamp = gtc()
progress_bar = gtc()
progress_label = gtc()
button = gtc()
def __init__(self, title, description, show_progress=False, on_cancel=None, **kwargs):
super().__init__(**kwargs)
self.title_label.set_label(GLib.markup_escape_text(title))
self.description_label.set_label(GLib.markup_escape_text(description))
self.progress_clamp.set_visible(show_progress)
if on_cancel is None:
self.button.set_visible(False)
else:
self.button.connect("clicked", lambda *_: on_cancel())

19
src/gtk/sidebar_button.py Normal file
View File

@@ -0,0 +1,19 @@
from gi.repository import Gtk
from .host_info import HostInfo
class SidebarButton(Gtk.Button):
__gtype_name__ = "SidebarButton"
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
main_split = HostInfo.main_window.main_split
# Connections
main_split.connect("notify::collapsed", lambda *_: self.set_visible(main_split.get_collapsed()))
self.connect("clicked", lambda *_: main_split.set_show_sidebar(True))
# Apply
self.set_icon_name("dock-left-symbolic")
self.set_tooltip_text(_("Show Sidebar"))

384
src/host_info.py Normal file
View File

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

View File

@@ -0,0 +1,41 @@
using Gtk 4.0;
using Adw 1;
template $FileInstallDialog : Adw.Dialog {
follows-content-size: true;
Adw.ToolbarView {
[top]
Adw.HeaderBar {
show-start-title-buttons: false;
show-end-title-buttons: false;
[start]
Button cancel_button {
label: _("Cancel");
}
[end]
Button apply_button {
styles ["suggested-action"]
label: _("Install");
}
}
ScrolledWindow content_page {
propagate-natural-height: true;
propagate-natural-width: true;
Adw.Clamp {
margin-start: 12;
margin-end: 12;
margin-bottom: 12;
margin-top: 6;
Box {
orientation: vertical;
spacing: 12;
Adw.PreferencesGroup packages_group {
title: _("Review Selection");
}
$InstallationChooser installation_chooser {
}
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
from gi.repository import Adw, Gtk
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/install_page/file_install_dialog.ui")
class FileInstallDialog(Adw.Dialog):
__gtype_name__ = "FileInstallDialog"
gtc = Gtk.Template.Child
packages_group = gtc()
installation_chooser = gtc()
cancel_button = gtc()
apply_button = gtc()
def generate_list(self):
for file in self.files:
row = Adw.ActionRow(title=file.get_basename())
row.add_prefix(Gtk.Image(icon_name="flatpak-symbolic"))
self.packages_group.add(row)
def on_response(self, *args):
self.on_add(self.installation_chooser.get_installation(), self.files)
self.close()
def __init__(self, parent_page, files, on_add, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
self.parent_page = parent_page
self.files = files
self.on_add = on_add
# Apply
self.generate_list()
if len(files) > 1:
self.set_title(_("Install Packages"))
# self.packages_group.set_title(_("Review Packages"))
self.packages_group.set_description(_("The following packages will be installed"))
self.installation_chooser.set_content_strings(_("Packages"), True)
else:
self.set_title(_("Install a Package"))
# self.packages_group.set_title(_("Review Package"))
self.packages_group.set_description(_("The following package will be installed"))
self.installation_chooser.set_content_strings(_("package"), False)
# Connections
self.cancel_button.connect("clicked", lambda *_: self.close())
self.apply_button.connect("clicked", self.on_response)

View File

@@ -0,0 +1,107 @@
using Gtk 4.0;
using Adw 1;
template $InstallPage : Adw.BreakpointBin {
width-request: 1;
height-request: 1;
Adw.Breakpoint break_point {
condition ("max-width: 600")
setters {
multi_view.layout: skinny;
}
}
Adw.NavigationPage {
title: _("Install Packages");
Adw.ToastOverlay toast_overlay {
Stack status_stack {
Adw.ToolbarView loading_view {
[top]
Adw.HeaderBar {
[start]
$SidebarButton {}
}
}
Adw.ToolbarView installing_view {
[top]
Adw.HeaderBar {
[start]
$SidebarButton {}
}
}
Adw.MultiLayoutView multi_view {
Adw.Layout wide {
Adw.NavigationSplitView split_view {
sidebar-width-fraction: 0.5;
max-sidebar-width: 999999999;
sidebar:
Adw.NavigationPage {
title: _("Select Source");
Adw.LayoutSlot {
id: "select_page";
}
}
;
content:
Adw.NavigationPage {
title: _("Pending Packages");
Adw.LayoutSlot {
id: "pending_page";
}
}
;
}
}
Adw.Layout skinny {
Adw.BottomSheet bottom_sheet {
[content]
Box {
margin-bottom: bind bottom_sheet.bottom-bar-height;
Adw.LayoutSlot {
id: "select_page";
}
}
[sheet]
Adw.LayoutSlot {
id: "pending_page";
}
}
}
[select_page]
$SelectPage select_page {}
[pending_page]
$PendingPage pending_page {}
}
}
}
}
}
Revealer bottom_child {
reveal-child: false;
Box {
margin-top: 12;
margin-bottom: 14;
spacing: 12;
halign: center;
valign: center;
styles ["flat"]
[start]
Image {
icon-name: "flatpak-symbolic";
icon-size: normal;
}
[center]
Label bottom_label {
label: _("Pending Packages");
styles ["heading"]
}
[end]
Image {
icon-name: "right-large-symbolic";
icon-size: normal;
}
}
}

View File

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

View File

@@ -0,0 +1,41 @@
using Gtk 4.0;
using Adw 1;
template $PendingPage : Adw.NavigationPage {
title: _("Pending Packages");
Stack stack {
Adw.ToolbarView none_pending {
[top]
Adw.HeaderBar {
}
Adw.StatusPage {
icon-name: "flatpak-symbolic";
title: _("Add Packages");
description: _("Packages queued to install will show up here");
}
}
Adw.ToolbarView main_view {
[top]
Adw.HeaderBar {
}
Adw.PreferencesPage preferences_page {
}
[bottom]
ActionBar pending_action_bar {
revealed: true;
[center]
Button install_button {
margin-top: 3;
margin-bottom: 3;
sensitive: bind pending_action_bar.revealed;
styles ["pill", "suggested-action"]
Adw.ButtonContent {
can-shrink: true;
icon-name: "arrow-pointing-at-line-down-symbolic";
label: _("Install");
}
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,49 @@
using Gtk 4.0;
using Adw 1;
template $ResultRow : Adw.ActionRow {
activatable: true;
title: "No title set";
subtitle: "No subtitle set";
tooltip-text: _("Add Package to Queue");
Box {
orientation: vertical;
valign: center;
spacing: 4;
margin-end: 4;
Label version_label {
styles ["subtitle"]
label: "";
justify: right;
halign: end;
hexpand: true;
wrap: true;
}
Label branch_label {
styles ["subtitle"]
label: "";
justify: right;
halign: end;
hexpand: true;
wrap: true;
}
}
[suffix]
Image add_image {
icon-name: "plus-large-symbolic";
}
[suffix]
Image selected_image {
icon-name: "check-plain-symbolic";
}
[suffix]
Image sub_image {
icon-name: "minus-large-symbolic";
}
[suffix]
Image installed_image {
icon-name: "selection-mode-symbolic";
styles ["success"]
}
}

View File

@@ -0,0 +1,71 @@
from gi.repository import Adw, Gtk, GLib
from enum import Enum
import os
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/install_page/result_row.ui")
class ResultRow(Adw.ActionRow):
__gtype_name__ = "ResultRow"
gtc = Gtk.Template.Child
version_label = gtc()
branch_label = gtc()
add_image = gtc()
sub_image = gtc()
selected_image = gtc()
installed_image = gtc()
class PackageState(Enum):
NEW = 0
SELECTED = 1
ADDED = 2
INSTALLED = 3
def idle_stuff(self):
self.set_title(GLib.markup_escape_text(self.package.name))
self.set_subtitle(self.package.app_id)
self.version_label.set_label(GLib.markup_escape_text(self.package.version))
self.branch_label.set_label(GLib.markup_escape_text(self.package.branch))
self.version_label.set_visible(len(self.version_label.get_label()) != 0)
self.branch_label.set_visible(len(self.branch_label.get_label()) != 0)
def set_state(self, state):
if state == self.state:
return
self.state = state
self.add_image.set_visible(False)
self.sub_image.set_visible(False)
self.selected_image.set_visible(False)
self.installed_image.set_visible(False)
match state:
case self.PackageState.NEW:
self.set_sensitive(True)
self.set_tooltip_text(_("Add Package to Queue"))
self.add_image.set_visible(True)
case self.PackageState.SELECTED:
self.set_sensitive(False)
self.set_tooltip_text(_("Package has been Added to Queue"))
self.selected_image.set_visible(True)
case self.PackageState.ADDED:
self.set_sensitive(True)
self.set_tooltip_text(_("Remove Package from Queue"))
self.sub_image.set_visible(True)
case self.PackageState.INSTALLED:
self.set_sensitive(False)
self.set_tooltip_text(_("This Package is Already Installed"))
self.installed_image.set_visible(True)
def __init__(self, package, package_state, origin_list_box, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
self.state = None
self.package = package
self.origin_list_box = origin_list_box
# Connections
# Apply
GLib.idle_add(self.idle_stuff)
self.set_state(package_state)

View File

@@ -0,0 +1,54 @@
using Gtk 4.0;
using Adw 1;
template $ResultsPage : Adw.NavigationPage {
title: _("Search a Remote");
Adw.ToolbarView {
[top]
Adw.HeaderBar {}
[top]
Adw.Clamp {
maximum-size: 577;
margin-top: 3;
margin-bottom: 3;
margin-start: 6;
margin-end: 6;
SearchEntry search_entry {
search-delay: 500;
halign: fill;
// hexpand: true;
placeholder-text: _("Search for Packages");
}
}
Stack stack {
Adw.StatusPage new_search {
icon-name: "loupe-large-symbolic";
title: _("Search for Flatpaks");
description: _("Search for Flatpaks you want to install");
}
Adw.StatusPage too_many {
icon-name: "error-symbolic";
title: _("Too Many Results");
description: _("Try being more specific with your search");
}
ScrolledWindow results_view {
Adw.Clamp {
ListBox results_list {
margin-start: 12;
margin-end: 12;
margin-top: 12;
margin-bottom: 12;
styles ["boxed-list"]
selection-mode: none;
valign: start;
}
}
}
Adw.StatusPage no_results {
icon-name: "loupe-large-symbolic";
title: _("No Results Found");
description: _("Try a different search term");
}
}
}
}

View File

@@ -0,0 +1,142 @@
from gi.repository import Adw, Gtk, GLib, Gio
from .host_info import HostInfo
from .result_row import ResultRow
from .loading_status import LoadingStatus
from .error_toast import ErrorToast
import subprocess
class AddedPackage:
def __eq__(self, other):
return (
self.name == other.name
and self.app_id == other.app_id
and self.branch == other.branch
and self.version == other.version
and self.remote == other.remote
and self.installation == other.installation
)
def is_similar(self, other):
return (
self.app_id == other.app_id
and self.branch == other.branch
and self.version == other.version
)
def __init__(self, name, app_id, branch, version, remote, installation):
self.name = name
self.app_id = app_id
self.branch = branch
self.version = version
self.remote = remote
self.installation = installation
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/install_page/results_page.ui")
class ResultsPage(Adw.NavigationPage):
__gtype_name__ = "ResultsPage"
gtc = Gtk.Template.Child
search_entry = gtc()
results_list = gtc()
stack = gtc()
new_search = gtc()
too_many = gtc()
results_view= gtc()
no_results = gtc()
def show_remote(self, row, remote, installation, nav_view=None):
self.remote = remote
self.installation = installation
self.set_title(_("Search {}").format(remote.title))
self.search_entry.set_text("")
self.search_entry.grab_focus()
if nav_view:
nav_view.push(self)
def add_package_row(self, row):
self.pending_page.add_package_row(row)
if not self.install_page is None:
self.install_page.package_added()
def on_search(self, *args):
self.packages.clear()
self.stack.set_visible_child(self.loading)
self.results_list.remove_all()
search_text = self.search_entry.get_text()
if search_text == "":
self.stack.set_visible_child(self.new_search)
return
def thread(*args):
installation = ""
if self.installation == "user" or self.installation == "system":
installation = f"--{self.installation}"
else:
installation = f"--installation={self.installation}"
try:
output = subprocess.run(
['flatpak-spawn', '--host', 'flatpak', 'search', '--columns=all', installation, self.search_entry.get_text()],
check=True, text=True, capture_output=True
).stdout.split('\n')
if len(output) > 100:
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.too_many))
return
for line in output:
line = line.strip()
info = line.split('\t')
if len(info) != 6:
continue
remotes = info[5].split(',')
if not self.remote.name in remotes:
continue
package = AddedPackage(info[0], info[2], info[4], info[3], self.remote, self.installation)
row = ResultRow(package, ResultRow.PackageState.NEW, self.results_list)
for item in self.pending_page.added_packages:
if package.is_similar(item):
row.set_state(ResultRow.PackageState.SELECTED)
if package.app_id in HostInfo.id_to_flatpak:
installed_package = HostInfo.id_to_flatpak[package.app_id]
if installed_package.info["id"] == package.app_id and installed_package.info["branch"] == package.branch:
row.set_state(ResultRow.PackageState.INSTALLED)
row.connect("activated", self.add_package_row)
self.packages.append(package)
GLib.idle_add(lambda *_, _row=row: self.results_list.append(_row))
if len(self.packages) > 0:
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.results_view))
else:
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.no_results))
except subprocess.CalledProcessError as cpe:
GLib.idle_add(lambda *_, cpe=cpe: HostInfo.main_window.toast_overlay.add_toast(ErrorToast("Could not search for package", cpe.stderr).toast))
GLib.idle_add(lambda *_: self.install_page.select_page.nav_view.pop())
Gio.Task().run_in_thread(thread)
def on_back(self, *args):
self.results_list.remove_all()
self.stack.set_visible_child(self.new_search)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
self.remote = None
self.installation = None
self.packages = []
self.pending_page = None
self.loading = LoadingStatus(_("Searching"), _("This should only take a moment"))
self.install_page = None
# Connections
self.search_entry.connect("search-changed", self.on_search)
# Apply
self.stack.add_child(self.loading)

View File

@@ -0,0 +1,50 @@
using Gtk 4.0;
using Adw 1;
template $SelectPage : Adw.NavigationPage {
title: _("Install Packages");
Adw.ToastOverlay toast_overlay {
Adw.NavigationView nav_view {
Adw.NavigationPage select_nav_page {
title: _("Install Packages");
Adw.ToolbarView {
[top]
Adw.HeaderBar {
[start]
$SidebarButton {}
}
Adw.PreferencesPage {
Adw.PreferencesGroup remotes_group {
title: _("Search in a Remote");
description: _("Choose a remote to search for new packages");
}
Adw.PreferencesGroup no_remotes {
title: _("Online Searches Disabled");
description: _("Your system has no remotes added to search from");
visible: bind remotes_group.visible inverted;
Adw.ActionRow add_remote_row {
title: _("Add a Remote");
subtitle: _("Add a remote to your system to enable online searching");
activatable: true;
[suffix]
Image {
icon-name: "right-large-symbolic";
}
}
}
Adw.PreferencesGroup local_group {
title: _("Add Packages");
description: _("Install packages from files on your system");
Adw.ButtonRow open_row {
title: _("Open Files");
start-icon-name: "folder-open-symbolic";
}
}
}
}
}
$ResultsPage results_page {}
}
}
}

View File

@@ -0,0 +1,91 @@
from gi.repository import Adw, Gtk, GLib, Gio
from .host_info import HostInfo
from .error_toast import ErrorToast
from .results_page import ResultsPage
from .sidebar_button import SidebarButton
from .file_install_dialog import FileInstallDialog
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/install_page/select_page.ui")
class SelectPage(Adw.NavigationPage):
__gtype_name__ = "SelectPage"
gtc = Gtk.Template.Child
nav_view = gtc()
results_page = gtc()
remotes_group = gtc()
add_remote_row = gtc()
open_row = gtc()
def start_loading(self):
self.nav_view.pop()
for row in self.remote_rows:
self.remotes_group.remove(row)
self.remote_rows.clear()
def end_loading(self):
for installation, remotes in HostInfo.remotes.items():
for remote in remotes:
if remote.disabled:
continue
row = Adw.ActionRow(title=remote.title, subtitle=_("Installation: {}").format(installation), activatable=True)
row.add_suffix(Gtk.Image(icon_name="right-large-symbolic"))
row.connect("activated", self.results_page.show_remote, remote, installation, self.nav_view)
self.remotes_group.add(row)
self.remote_rows.append(row)
self.remotes_group.set_visible(len(self.remote_rows) != 0)
def local_install_apply_callback(self, installation, file_names):
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
requests = []
for file in file_names:
# sadly flatpak doesn't support multiple local installs in one command :(
requests.append({
"remote": "local_file",
"installation": installation,
"package_names": [file.get_path()],
"extra_flags": [],
})
install_page.install_packages(requests)
def file_dialog_handler(self, files):
FileInstallDialog(self, files, self.local_install_apply_callback).present(HostInfo.main_window)
def file_choose_callback(self, object, result):
try:
files = object.open_multiple_finish(result)
if not files:
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), _("No files were found to install")))
return
self.file_dialog_handler(files)
except GLib.GError as gle:
if not (gle.domain == "gtk-dialog-error-quark" and gle.code == 2):
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), str(gle)).toast)
def on_open(self, *args):
file_filter = Gtk.FileFilter(name=_("Flatpaks"))
file_filter.add_suffix("flatpak")
file_filter.add_suffix("flatpakref")
filters = Gio.ListStore.new(Gtk.FileFilter)
filters.append(file_filter)
file_chooser = Gtk.FileDialog()
file_chooser.set_filters(filters)
file_chooser.set_default_filter(file_filter)
file_chooser.open_multiple(HostInfo.main_window, None, self.file_choose_callback)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
self.remote_rows = []
# Connections
self.add_remote_row.connect("activated", lambda *_: HostInfo.main_window.activate_row(HostInfo.main_window.remotes_row))
self.nav_view.connect("popped", self.results_page.on_back)
self.open_row.connect("activated", self.on_open)
# Apply

View File

@@ -26,57 +26,45 @@ gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Gio, Adw, GLib
from .host_info import HostInfo
from .window import WarehouseWindow
from .remotes_window import RemotesWindow
from .orphans_window import OrphansWindow
from .filter_window import FilterWindow
from .search_install_window import SearchInstallWindow
from .const import Config
from .common import myUtils
from .error_toast import ErrorToast
class WarehouseApplication(Adw.Application):
"""The main application singleton class."""
troubleshooting = "OS: {os}\nWarehouse version: {wv}\nGTK: {gtk}\nlibadwaita: {adw}\nApp ID: {app_id}\nProfile: {profile}\nLanguage: {lang}"
version = Config.VERSION
def __init__(self):
super().__init__(
application_id="io.github.flattool.Warehouse",
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
)
self.create_action("quit", lambda *_: self.quit(), ["<primary>q"])
self.create_action("about", self.on_about_action)
self.create_action("preferences", self.on_preferences_action)
self.create_action("search", self.on_search_action, ["<primary>f"])
self.create_action("manage-data-folders", self.manage_data_shortcut)
self.create_action(
"toggle-batch-mode",
self.batch_mode_shortcut,
["<primary>b", "<primary>Return"],
)
self.create_action(
"toggle-batch-mode-keypad", self.batch_mode_shortcut, ["<primary>KP_Enter"]
) # This action is not added to the shortcuts window
self.create_action(
"manage-data-folders", self.manage_data_shortcut, ["<primary>d"]
)
self.create_action(
"refresh-list", self.refresh_list_shortcut, ["<primary>r", "F5"]
)
self.create_action(
"show-remotes-window", self.show_remotes_shortcut, ["<primary>m"]
)
self.create_action("set-filter", self.filters_shortcut, ["<primary>t"])
self.create_action("install-from-file", self.install_from_file, ["<primary>o"])
self.create_action("open-menu", self.main_menu_shortcut, ["F10"])
self.create_action(
"open-search-install", self.open_search_install, ["<primary>i"]
)
self.create_action("quit", lambda *_: self.quit(), ["<primary>q"])
self.create_action("open-menu", lambda *_: self.props.active_window.main_menu.popup(), ["F10"])
self.create_action("refresh", lambda *_: self.props.active_window.refresh_handler(), ["<primary>r", "F5"])
self.create_action("open-files", self.on_open_files_shortcut, ["<primary>o"])
self.create_action("show-packages-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("p"), ["<primary>p"])
self.create_action("show-remotes-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("m"), ["<primary>m"])
self.create_action("show-user-data-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("d"), ["<primary>d"])
self.create_action("show-snapshots-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("s"), ["<primary>s"])
self.create_action("show-install-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("i"), ["<primary>i"])
self.create_action("toggle-select-mode", self.on_toggle_select_mode_shortcut, ["<primary>b", "<primary>Return"])
self.create_action("toggle-selection-kp-enter", self.on_toggle_select_mode_shortcut, ["<primary>KP_Enter"]) # Doesn't show in the shortcuts window
self.create_action("search-mode", self.on_search_mode_shortcut, ["<primary>f"])
self.create_action("filter", self.on_filter_shortcut, ["<primary>t"])
self.create_action("new", self.on_new_shortcut, ["<primary>n"])
self.create_action("active-data-view", lambda *_: self.on_data_view_shortcut(True), ["<Alt>1"])
self.create_action("leftover-data-view", lambda *_: self.on_data_view_shortcut(False), ["<Alt>2"])
self.is_dialog_open = False
gtk_version = (
str(Gtk.MAJOR_VERSION)
+ "."
@@ -93,7 +81,7 @@ class WarehouseApplication(Adw.Application):
)
os_string = GLib.get_os_info("NAME") + " " + GLib.get_os_info("VERSION")
lang = GLib.environ_getenv(GLib.get_environ(), "LANG")
self.troubleshooting = self.troubleshooting.format(
os=os_string,
wv=self.version,
@@ -103,62 +91,101 @@ class WarehouseApplication(Adw.Application):
app_id=self.get_application_id(),
lang=lang,
)
self.my_utils = myUtils(self)
total = 0
for rem in self.my_utils.get_host_remotes():
if self.my_utils.get_install_type(rem[7]) != "disabled":
total += 1
if total < 1:
self.lookup_action(f"open-search-install").set_enabled(False)
def open_search_install(self, widget, _):
SearchInstallWindow(self.props.active_window)
def batch_mode_shortcut(self, widget, _):
button = self.props.active_window.batch_mode_button
button.set_active(not button.get_active())
def manage_data_shortcut(self, widget, _):
OrphansWindow(self.props.active_window)
def refresh_list_shortcut(self, widget, _):
self.props.active_window.refresh_list_of_flatpaks(widget)
def show_remotes_shortcut(self, widget, _):
RemotesWindow(self.props.active_window)
def filters_shortcut(self, widget, _):
FilterWindow(self.props.active_window)
def main_menu_shortcut(self, widget, _):
def on_open_files_shortcut(self, *args):
window = self.props.active_window
window.main_menu.set_active(True)
def file_callback(self, object, result):
window = self.props.active_window
try:
file = object.open_finish(result)
window.install_file(file.get_path())
except GLib.GError:
pass
def install_from_file(self, widget, _a):
window = self.props.active_window
filter = Gtk.FileFilter(name=_("Flatpaks"))
filter.add_suffix("flatpak")
filter.add_suffix("flatpakref")
def file_choose_callback(object, result):
try:
files = object.open_multiple_finish(result)
if not files:
window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), _("No files were found")).toast)
return
window.on_file_drop(None, files, None, None)
except GLib.GError as gle:
if not (gle.domain == "gtk-dialog-error-quark" and gle.code == 2):
window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), str(gle)).toast)
file_filter = Gtk.FileFilter(name=_("Flatpaks & Remotes"))
file_filter.add_suffix("flatpak")
file_filter.add_suffix("flatpakref")
file_filter.add_suffix("flatpakrepo")
filters = Gio.ListStore.new(Gtk.FileFilter)
filters.append(filter)
filters.append(file_filter)
file_chooser = Gtk.FileDialog()
file_chooser.set_filters(filters)
file_chooser.set_default_filter(filter)
file_chooser.open(window, None, self.file_callback)
file_chooser.set_default_filter(file_filter)
file_chooser.open_multiple(window, None, file_choose_callback)
def on_toggle_select_mode_shortcut(self, *args):
try:
button = self.props.active_window.stack.get_visible_child().select_button
button.set_active(not button.get_active())
except AttributeError:
pass
def on_search_mode_shortcut(self, *args):
try:
button = self.props.active_window.stack.get_visible_child().search_button
button.set_active(True)
except AttributeError:
pass
def on_filter_shortcut(self, *args):
try:
button = self.props.active_window.stack.get_visible_child().filter_button
button.set_active(not button.get_active())
except AttributeError:
pass
try:
button = self.props.active_window.stack.get_visible_child().sort_button
button.set_active(True)
except AttributeError:
pass
try:
button = self.props.active_window.stack.get_visible_child().show_disabled_button
if button.get_visible():
button.set_active(not button.get_active())
except AttributeError:
pass
def on_new_shortcut(self, *args):
page = self.props.active_window.stack.get_visible_child()
try:
page.new_custom_handler()
except AttributeError:
pass
try:
page.on_new()
except AttributeError:
pass
def on_delete_shortcut(self, *args):
page = self.props.active_window.stack.get_visible_child()
try:
if not page.select_button.get_active():
return
page.select_trash_handler()
except AttributeError:
pass
def on_data_view_shortcut(self, is_active):
page = self.props.active_window.stack.get_visible_child()
try:
adp = page.adp
ldp = page.ldp
page.stack.set_visible_child(adp if is_active else ldp)
except AttributeError:
pass
def do_activate(self):
"""Called when the application is activated.
We raise the application's main window, creating it if
necessary.
"""
@@ -166,7 +193,7 @@ class WarehouseApplication(Adw.Application):
if not win:
win = WarehouseWindow(application=self)
win.present()
def on_about_action(self, widget, _a):
"""Callback for the app.about action."""
about = Adw.AboutDialog(
@@ -207,23 +234,17 @@ class WarehouseApplication(Adw.Application):
],
)
about.present(self.props.active_window)
def on_preferences_action(self, widget, _):
"""Callback for the app.preferences action."""
print("app.preferences action activated")
def on_search_action(self, widget, _):
self.props.active_window.search_bar.set_search_mode(
not self.props.active_window.search_bar.get_search_mode()
)
def create_action(self, name, callback, shortcuts=None):
"""Add an application action.
Args:
name: the name of the action
callback: the function to be called when the action is
activated
callback: the function to be called when the action is activated
shortcuts: an optional list of accelerators
"""
action = Gio.SimpleAction.new(name, None)
@@ -231,8 +252,7 @@ class WarehouseApplication(Adw.Application):
self.add_action(action)
if shortcuts:
self.set_accels_for_action(f"app.{name}", shortcuts)
def main(version):
"""The application's entry point."""
app = WarehouseApplication()

161
src/main_window/window.blp Normal file
View File

@@ -0,0 +1,161 @@
using Gtk 4.0;
using Adw 1;
template $WarehouseWindow: Adw.ApplicationWindow {
title: "Warehouse";
default-width: 921;
default-height: 450;
Adw.Breakpoint main_breakpoint {
condition ("min-width: 865")
setters {
main_split.collapsed: false;
main_split.max-sidebar-width: 999999999;
}
}
content:
Adw.ToastOverlay toast_overlay {
Overlay {
[overlay]
Revealer file_drop_revealer {
can-target: false;
transition-type: crossfade;
Adw.StatusPage file_drop_view {
icon-name: "folder-open-symbolic";
title: _("Drop to Open");
description: _("Install Flatpaks or Add a Remote");
styles ["drag-overlay-status-page"]
}
}
Adw.OverlaySplitView main_split {
collapsed: true;
show-sidebar: true;
sidebar-width-fraction: 0.2;
min-sidebar-width: 250;
sidebar:
Adw.NavigationPage {
title: "Warehouse";
Adw.ToolbarView main_toolbar_view {
[top]
Adw.HeaderBar header_bar {
[start]
Button refresh_button {
icon-name: "arrow-circular-top-right-symbolic";
tooltip-text: _("Refresh List");
}
[end]
MenuButton main_menu {
icon-name: "open-menu-symbolic";
tooltip-text: _("Main Menu");
menu-model: primary_menu;
}
}
content:
ScrolledWindow {
ListBox navigation_row_listbox {
styles ["navigation-sidebar"]
Box packages_row {
margin-top: 12;
margin-bottom: 12;
margin-start: 6;
margin-end: 6;
spacing: 12;
Image icon {
icon-name: "flatpak-symbolic";
}
Label {
label: _("Packages");
}
}
Box remotes_row {
margin-top: 12;
margin-bottom: 12;
margin-start: 6;
margin-end: 6;
spacing: 12;
Image {
icon-name: "server-pick-symbolic";
}
Label {
label: _("Remotes");
}
}
Box user_data_row {
margin-top: 12;
margin-bottom: 12;
margin-start: 6;
margin-end: 6;
spacing: 12;
Image {
icon-name: "file-manager-symbolic";
}
Label {
label: _("User Data");
}
}
Box snapshots_row {
margin-top: 12;
margin-bottom: 12;
margin-start: 6;
margin-end: 6;
spacing: 12;
Image {
icon-name: "snapshots-alt-symbolic";
}
Label {
label: _("Snapshots");
}
}
Box install_row {
margin-top: 12;
margin-bottom: 12;
margin-start: 6;
margin-end: 6;
spacing: 12;
Image {
icon-name: "arrow-pointing-at-line-down-symbolic";
}
Label {
label: _("Install Packages");
}
}
}
}
;
}
}
;
content:
Stack stack {
}
;
}
}
}
;
}
menu primary_menu {
section {
/*item {
label: _("_Preferences");
action: "app.preferences";
}*/
// item {
// label: _("Refresh List");
// action: "app.refresh-list";
// }
item {
label: _("_Open Files");
action: "app.open-files";
}
item {
label: _("_Keyboard Shortcuts");
action: "win.show-help-overlay";
}
item {
label: _("_About Warehouse");
action: "app.about";
}
}
}

254
src/main_window/window.py Normal file
View File

@@ -0,0 +1,254 @@
# window.py
#
# Copyright 2023 Heliguy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-only
from gi.repository import Adw, Gdk, Gio, GLib, Gtk
from .host_info import HostInfo
from .packages_page import PackagesPage
from .remotes_page import RemotesPage
from .user_data_page import UserDataPage
from .snapshot_page import SnapshotPage
from .install_page import InstallPage
from .error_toast import ErrorToast
from .const import Config
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/main_window/window.ui")
class WarehouseWindow(Adw.ApplicationWindow):
__gtype_name__ = "WarehouseWindow"
gtc = Gtk.Template.Child
main_breakpoint = gtc()
toast_overlay = gtc()
file_drop_revealer = gtc()
main_split = gtc()
file_drop_view = gtc()
stack = gtc()
refresh_button = gtc()
main_menu = gtc()
navigation_row_listbox = gtc()
packages_row = gtc()
remotes_row = gtc()
user_data_row = gtc()
snapshots_row = gtc()
install_row = gtc()
def start_loading(self, *args):
for _, page in self.pages.items():
if page.instance:
page.instance.start_loading()
def end_loading(self, *args):
for _, page in self.pages.items():
if page.instance:
page.instance.end_loading()
self.refresh_button.set_sensitive(True)
self.refresh_requested = False
self.remove_refresh_lockout("refresh handler direct")
def do_refresh(self):
self.start_loading()
self.refresh_button.set_sensitive(False)
HostInfo.get_flatpaks(callback=self.end_loading)
def refresh_handler(self, *args):
if len(self.refresh_lockouts) == 0:
self.add_refresh_lockout("refresh handler direct")
self.do_refresh()
elif "refresh handler direct" in self.refresh_lockouts:
return
else:
self.refresh_requested = True
def add_refresh_lockout(self, reason):
self.refresh_lockouts.append(reason)
self.refresh_button.set_sensitive(False)
def remove_refresh_lockout(self, reason):
if reason in self.refresh_lockouts:
self.refresh_lockouts.remove(reason)
if len(self.refresh_lockouts) == 0:
if self.refresh_requested:
self.do_refresh()
else:
self.refresh_button.set_sensitive(True)
def navigation_handler(self, _, row):
row = row.get_child()
page = self.pages[row]
self.stack.set_visible_child(page)
self.settings.set_string("page-shown", page.page_name)
if self.main_split.get_collapsed():
self.main_split.set_show_sidebar(False)
def activate_row(self, nav_row):
idx = 0
while row := self.navigation_row_listbox.get_row_at_index(idx):
idx += 1
if row.get_child() is nav_row:
row.activate()
nav_row.grab_focus()
break
def show_saved_page(self):
page_to_show = self.settings.get_string("page-shown")
page_found = False
for row, page in self.pages.items():
self.stack.add_child(page)
if page.page_name == page_to_show:
page_found = True
self.activate_row(row)
if not page_found:
self.navigation_row_listbox.get_row_at_index(0).activate()
def on_file_drop(self, drop_target, value, x, y):
try:
paks = []
remotes = []
for file in value:
path = file.get_path()
if path.endswith(".flatpak") or path.endswith(".flatpakref"):
paks.append(Gio.File.new_for_path(path))
elif path.endswith(".flatpakrepo"):
remotes.append(path)
else:
dialog = Adw.AlertDialog(
heading=_("Unsupported Filetype"),
body=_("Only .flatpak, .flatpakref, and .flatpakrepo files are supported."),
)
dialog.add_response("continue", _("OK"))
dialog.present(self)
return
if len(remotes) > 0 and len(paks) > 0:
dialog = Adw.AlertDialog(
heading=_("Mixed Filetypes"),
body=_("Flatpaks and remotes cannot be installed at the same time."),
)
dialog.add_css_class("error")
dialog.add_response("continue", _("OK"))
dialog.present(self)
return
if len(remotes) > 1:
dialog = Adw.AlertDialog(
heading=_("Too Many Remotes"),
body=_("Only one remote at a time is supported."),
)
dialog.add_response("continue", _("OK"))
dialog.present(self)
return
if len(remotes) == 1:
# Adding a remote
self.activate_row(self.remotes_row)
remotes_page = self.pages[self.remotes_row]
remotes_page.local_file_handler(remotes[0])
elif len(paks) > 0:
# Add packages
self.activate_row(self.install_row)
install_page = self.pages[self.install_row]
install_page.select_page.file_dialog_handler(paks)
except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not open files"), str(e)).toast)
def on_drop_enter(self, *args):
self.main_split.add_css_class("blurred")
self.file_drop_revealer.set_reveal_child(True)
return 1
def on_drop_leave(self, *args):
self.main_split.remove_css_class("blurred")
self.file_drop_revealer.set_reveal_child(False)
def switch_page_shortcut_handler(self, letter):
self.activate_row(self.shortcut_to_pages[letter])
def key_handler(self, controller, keyval, keycode, state):
page = self.stack.get_visible_child()
if keyval == Gdk.KEY_BackSpace or keyval == Gdk.KEY_Delete:
try:
if page.select_button.get_active():
page.on_backspace_handler()
except AttributeError:
pass
elif keyval == Gdk.KEY_Escape:
try:
page.on_escape_handler()
except AttributeError:
pass
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
HostInfo.main_window = self
ErrorToast.main_window = self
self.settings = Gio.Settings.new("io.github.flattool.Warehouse")
self.pages = {
self.packages_row: PackagesPage(main_window=self),
self.remotes_row: RemotesPage(main_window=self),
self.user_data_row: UserDataPage(main_window=self),
self.snapshots_row: SnapshotPage(main_window=self),
self.install_row: InstallPage(main_window=self),
}
self.shortcut_to_pages = {
"p": self.packages_row,
"m": self.remotes_row,
"d": self.user_data_row,
"s": self.snapshots_row,
"i": self.install_row,
}
self.navigation_row_listbox.connect("row-activated", self.navigation_handler)
self.show_saved_page()
self.refresh_lockouts = []
self.refresh_requested = False
file_drop = Gtk.DropTarget.new(Gdk.FileList, Gdk.DragAction.COPY)
event_controller = Gtk.EventControllerKey()
# Apply
self.add_controller(file_drop)
self.add_controller(event_controller)
self.settings.bind("window-width", self, "default-width", Gio.SettingsBindFlags.DEFAULT)
self.settings.bind("window-height", self, "default-height", Gio.SettingsBindFlags.DEFAULT)
self.settings.bind("is-maximized", self, "maximized", Gio.SettingsBindFlags.DEFAULT)
self.settings.bind("is-fullscreen", self, "fullscreened", Gio.SettingsBindFlags.DEFAULT)
# self.scrolled_window.add_controller(file_drop)
# self.main_split.set_content(PackagesPage(self))
if Config.DEVEL:
self.add_css_class("devel")
# Connections
file_drop.connect("drop", self.on_file_drop)
file_drop.connect("enter", self.on_drop_enter)
file_drop.connect("leave", self.on_drop_leave)
event_controller.connect("key-pressed", self.key_handler)
# file_drop.connect("drop", self.drop_callback)
self.refresh_button.connect("clicked", self.refresh_handler)
# self.activate_row(self.user_data_row)
# self.main_split.set_show_sidebar(self.settings.get_boolean("sidebar-shown"))
# GLib.idle_add(lambda *_: self.main_split.set_show_sidebar(False))
# print(self.settings.get_boolean("sidebar-shown"))
# self.main_split.connect("notify::show-sidebar", self.save_sidebar_state)
self.start_loading()
HostInfo.get_flatpaks(callback=self.end_loading)
# GLib.idle_add(lambda *_: self.main_split.set_show_sidebar(False))

View File

@@ -1,18 +1,36 @@
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
moduledir = join_paths(pkgdatadir, 'flattool_gui')
moduledir = join_paths(pkgdatadir, 'Warehouse')
gnome = import('gnome')
blueprints = custom_target('blueprints',
input: files(
'gtk/app_row.blp',
'gtk/help-overlay.blp',
'../data/ui/window.blp',
'../data/ui/orphans.blp',
'../data/ui/filter.blp',
'../data/ui/remotes.blp',
'../data/ui/downgrade.blp',
'../data/ui/search_install.blp',
'../data/ui/snapshots.blp',
'../data/ui/properties.blp',
'gtk/loading_status.blp',
'gtk/installation_chooser.blp',
'gtk/attempt_install_dialog.blp',
'main_window/window.blp',
'packages_page/packages_page.blp',
'packages_page/filters_page.blp',
'packages_page/uninstall_dialog.blp',
'properties_page/properties_page.blp',
'user_data_page/data_box.blp',
'user_data_page/user_data_page.blp',
'user_data_page/data_subpage.blp',
'remotes_page/remotes_page.blp',
'remotes_page/remote_row.blp',
'remotes_page/add_remote_dialog.blp',
'change_version_page/change_version_page.blp',
'snapshot_page/snapshot_page.blp',
'snapshot_page/snapshots_list_page.blp',
'snapshot_page/snapshot_box.blp',
'snapshot_page/new_snapshot_dialog.blp',
'install_page/file_install_dialog.blp',
'install_page/install_page.blp',
'install_page/result_row.blp',
'install_page/select_page.blp',
'install_page/results_page.blp',
'install_page/pending_page.blp',
),
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
@@ -54,31 +72,39 @@ configure_file(
warehouse_sources = [
'__init__.py',
'main.py',
'common.py',
'window.py',
'app_row_widget.py',
'host_info.py',
'gtk/error_toast.py',
'gtk/sidebar_button.py',
'gtk/loading_status.py',
'gtk/installation_chooser.py',
'gtk/app_row.py',
'gtk/attempt_install_dialog.py',
'main_window/window.py',
'package_install_worker.py',
'packages_page/uninstall_dialog.py',
'packages_page/packages_page.py',
'packages_page/filters_page.py',
'properties_page/properties_page.py',
'change_version_page/change_version_page.py',
'change_version_page/change_version_worker.py',
'user_data_page/data_box.py',
'user_data_page/user_data_page.py',
'user_data_page/data_subpage.py',
'remotes_page/remotes_page.py',
'remotes_page/remote_row.py',
'remotes_page/add_remote_dialog.py',
'snapshot_page/tar_worker.py',
'snapshot_page/snapshot_page.py',
'snapshot_page/snapshots_list_page.py',
'snapshot_page/snapshot_box.py',
'snapshot_page/new_snapshot_dialog.py',
'install_page/file_install_dialog.py',
'install_page/install_page.py',
'install_page/result_row.py',
'install_page/select_page.py',
'install_page/results_page.py',
'install_page/pending_page.py',
'../data/style.css',
'properties_window.py',
'../data/ui/properties.blp',
'orphans_window.py',
'../data/ui/orphans.blp',
'remotes_window.py',
'../data/ui/remotes.blp',
'filter_window.py',
'../data/ui/filter.blp',
'downgrade_window.py',
'../data/ui/downgrade.blp',
'search_install_window.py',
'../data/ui/search_install.blp',
'snapshots_window.py',
'../data/ui/snapshots.blp',
]
configure_file(

View File

@@ -1,353 +0,0 @@
from gi.repository import Gtk, Adw, GLib, Gdk, Gio
from .common import myUtils
import subprocess
import os
import pathlib
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/../data/ui/orphans.ui")
class OrphansWindow(Adw.Dialog):
__gtype_name__ = "OrphansWindow"
list_of_data = Gtk.Template.Child()
install_button = Gtk.Template.Child()
trash_button = Gtk.Template.Child()
select_all_button = Gtk.Template.Child()
main_overlay = Gtk.Template.Child()
toast_overlay = Gtk.Template.Child()
main_stack = Gtk.Template.Child()
no_data = Gtk.Template.Child()
no_results = Gtk.Template.Child()
action_bar = Gtk.Template.Child()
search_button = Gtk.Template.Child()
search_bar = Gtk.Template.Child()
search_entry = Gtk.Template.Child()
oepn_folder_button = Gtk.Template.Child()
installing = Gtk.Template.Child()
main_box = Gtk.Template.Child()
installing_status = Gtk.Template.Child()
window_title = _("Manage Leftover Data")
host_home = str(pathlib.Path.home())
user_data_path = host_home + "/.var/app/"
should_select_all = False
selected_remote = ""
selected_remote_install_type = ""
is_result = False
is_installing = False
is_open = False
def key_handler(self, controller, keyval, keycode, state):
if keyval == Gdk.KEY_Escape or (
keyval == Gdk.KEY_w and state == Gdk.ModifierType.CONTROL_MASK
):
self.close()
def selection_handler(self, widget, dir_name):
if widget.get_active():
self.selected_dirs.append(dir_name)
else:
self.selected_dirs.remove(dir_name)
if len(self.selected_dirs) == 0:
self.set_title(
self.window_title
) # Set the window title back to what it was when there are no selected dirs
else:
self.set_title(
("{} selected").format(str(len(self.selected_dirs)))
) # Set the window title to the amount of selected dirs
if len(self.selected_dirs) == 0:
self.install_button.set_sensitive(False)
self.trash_button.set_sensitive(False)
else:
self.install_button.set_sensitive(True)
self.trash_button.set_sensitive(True)
def select_all_handler(self, button):
for check in self.check_buttons:
check.set_active(button.get_active())
def install_callback(self, *_args):
self.is_installing = False
self.generate_list()
self.progress_bar.set_visible(False)
self.app_window.refresh_list_of_flatpaks(self)
self.set_can_close(True) # Make window able to close
self.search_button.set_sensitive(True)
if self.my_utils.install_success:
self.toast_overlay.add_toast(Adw.Toast.new(_("Installed successfully")))
else:
self.toast_overlay.add_toast(
Adw.Toast.new(_("Could not install some apps"))
)
def install_handler(self):
self.is_installing = True
self.main_stack.set_visible_child(self.installing)
self.search_button.set_sensitive(False)
self.set_title(self.window_title)
self.keep_checking = True
task = Gio.Task.new(None, None, self.install_callback)
task.run_in_thread(
lambda _task, _obj, _data, _cancellable, id_list=self.selected_dirs, remote=self.selected_remote, app_type=self.selected_remote_type, progress_bar=self.progress_bar, status_label=self.installing_status: self.my_utils.install_flatpak(
id_list, remote, app_type, progress_bar, status_label
)
)
def install_button_handler(self, button):
def remote_select_handler(button, index):
if not button.get_active():
return
self.selected_remote = self.host_remotes[index][0]
self.selected_remote_type = self.my_utils.get_install_type(
self.host_remotes[index][7]
)
def on_response(dialog, response_id, _function):
if response_id == "cancel":
return
self.install_handler()
self.progress_bar.set_visible(True)
self.action_bar.set_visible(False)
self.set_can_close(False) # Make window unable to close
dialog = Adw.AlertDialog.new(
_("Attempt to Install?"),
_("Warehouse will attempt to install apps matching the selected data."),
)
dialog.set_close_response("cancel")
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Install"))
dialog.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED)
height = 65 * len(self.host_remotes)
max = 400
if height > max:
height = max
remotes_scroll = Gtk.ScrolledWindow(vexpand=True, min_content_height=height)
remote_list = Gtk.ListBox(selection_mode="none", valign="start")
remotes_scroll.set_child(remote_list)
remote_list.add_css_class("boxed-list")
total_added = 0
remote_select_buttons = []
for i in range(len(self.host_remotes)):
title = self.host_remotes[i][1]
name = self.host_remotes[i][0]
type_arr = self.host_remotes[i][7]
if "disabled" in type_arr:
continue
remote_row = Adw.ActionRow(title=title)
remote_select = Gtk.CheckButton()
remote_select_buttons.append(remote_select)
remote_select.connect("toggled", remote_select_handler, i)
remote_row.set_activatable_widget(remote_select)
type = self.my_utils.get_install_type(type_arr)
if type == "user":
remote_row.set_subtitle(_("User wide"))
elif type == "system":
remote_row.set_subtitle(_("System wide"))
else:
remote_row.set_subtitle(_("Unknown install type"))
if remote_row.get_title() == "-":
remote_row.set_title(self.host_remotes[i][0])
if total_added > 0:
remote_select.set_group(remote_select_buttons[0])
remote_row.add_prefix(remote_select)
remote_list.append(remote_row)
total_added += 1
remote_select_buttons[0].set_active(True)
if total_added > 1:
dialog.set_extra_child(remotes_scroll)
dialog.connect("response", on_response, dialog.choose_finish)
dialog.present(self)
def trash_handler(self, button):
def on_response(dialog, response_id, _function):
if response_id == "cancel":
return
for i in range(len(self.selected_dirs)):
path = self.user_data_path + self.selected_dirs[i]
self.my_utils.trash_folder(path)
self.select_all_button.set_active(False)
self.generate_list()
dialog = Adw.AlertDialog.new(
_("Trash folders?"), _("These folders will be sent to the trash.")
)
dialog.connect("response", on_response, dialog.choose_finish)
dialog.set_close_response("cancel")
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Continue"))
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.present(self)
def open_button_handler(self, _widget, path=user_data_path):
try:
Gio.AppInfo.launch_default_for_uri(f"file://{path}", None)
except GLib.GError:
selt.toast_overlay.add_toast(Adw.Toast.new(_("Could not open folder")))
def size_callback(self, row_index):
row = self.list_of_data.get_row_at_index(row_index)
row.set_subtitle(f"~{self.data_rows[row_index][1]}")
def size_thread(self, index, path):
size = self.my_utils.get_size_with_format(path)
self.data_rows[index].append(size)
# Create the list of folders in the window
def generate_list(self):
self.data_rows = []
self.check_buttons = []
self.host_flatpaks = self.my_utils.get_host_flatpaks()
if self.host_flatpaks == [["", ""]]:
self.app_window.toast_overlay.add_toast(
Adw.Toast.new(_("Could not manage data"))
)
self.this_just_crashes_the_window_so_it_doesnt_open()
return
self.list_of_data.remove_all()
self.selected_dirs = []
self.set_title(self.window_title)
dir_list = os.listdir(self.user_data_path)
# This is a list that only holds IDs of install flatpaks
id_list = []
for i in range(len(self.host_flatpaks)):
try:
id_list.append(self.host_flatpaks[i][2])
except:
print("Could not get data")
for i in range(len(dir_list)):
dir_name = dir_list[i]
# Skip item if it has a matching flatpak
if dir_name in id_list:
continue
# Create row element
dir_row = Adw.ActionRow(title=dir_name)
self.data_rows.append([dir_row])
path = self.user_data_path + dir_name
index = len(self.data_rows) - 1
task = Gio.Task.new(
None, None, lambda *_, index=index: self.size_callback(index)
)
task.run_in_thread(
lambda _task, _obj, _data, _cancellable, *_, index=index: self.size_thread(
index, path
)
)
open_row_button = Gtk.Button(
icon_name="document-open-symbolic",
valign=Gtk.Align.CENTER,
tooltip_text=_("Open User Data Folder"),
)
open_row_button.add_css_class("flat")
open_row_button.connect(
"clicked", self.open_button_handler, (self.user_data_path + dir_name)
)
dir_row.add_suffix(open_row_button)
select_button = Gtk.CheckButton(tooltip_text=_("Select"))
self.check_buttons.append(select_button)
select_button.add_css_class("selection-mode")
select_button.connect("toggled", self.selection_handler, dir_name)
dir_row.add_suffix(select_button)
dir_row.set_activatable_widget(select_button)
# Add row to list
self.list_of_data.append(dir_row)
if self.list_of_data.get_row_at_index(0) == None:
self.main_stack.set_visible_child(self.no_data)
self.action_bar.set_visible(False)
else:
self.main_stack.set_visible_child(self.main_box)
self.action_bar.set_visible(True)
def filter_func(self, row):
if self.search_entry.get_text().lower() in row.get_title().lower():
self.is_result = True
return True
def on_invalidate(self, row):
if self.is_installing:
return
if self.list_of_data.get_row_at_index(0) == None:
self.main_stack.set_visible_child(self.no_data)
self.action_bar.set_visible(False)
else:
self.main_stack.set_visible_child(self.main_box)
self.action_bar.set_visible(True)
self.is_result = False
self.list_of_data.invalidate_filter()
if self.is_result == False:
self.main_stack.set_visible_child(self.no_results)
self.action_bar.set_visible(False)
def on_change(self, prop, prop2):
if self.is_installing:
return
if self.search_bar.get_search_mode() == False:
if self.list_of_data.get_row_at_index(0) == None:
self.main_stack.set_visible_child(self.no_data)
self.action_bar.set_visible(False)
else:
self.main_stack.set_visible_child(self.main_box)
self.action_bar.set_visible(True)
def __init__(self, main_window, **kwargs):
super().__init__(**kwargs)
self.my_utils = myUtils(
self
) # Access common utils and set the window to this window
self.host_remotes = self.my_utils.get_host_remotes()
self.host_flatpaks = self.my_utils.get_host_flatpaks()
self.progress_bar = Gtk.ProgressBar(visible=False)
self.progress_bar.add_css_class("osd")
self.app_window = main_window
self.generate_list()
event_controller = Gtk.EventControllerKey()
event_controller.connect("key-pressed", self.key_handler)
self.add_controller(event_controller)
self.install_button.connect("clicked", self.install_button_handler)
if self.host_remotes[0][0] == "":
self.install_button.set_visible(False)
self.trash_button.connect("clicked", self.trash_handler)
self.select_all_button.connect("toggled", self.select_all_handler)
self.main_overlay.add_overlay(self.progress_bar)
self.list_of_data.set_filter_func(self.filter_func)
self.search_entry.connect("search-changed", self.on_invalidate)
self.search_bar.connect("notify", self.on_change)
self.search_bar.connect_entry(self.search_entry)
self.oepn_folder_button.connect("clicked", self.open_button_handler)
def set_is_open_false(*args):
self.__class__.is_open = False
self.connect("closed", set_is_open_false)
if self.__class__.is_open:
return
else:
self.present(main_window)
self.__class__.is_open = True

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More