22
README.md
@@ -2,21 +2,23 @@
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
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
|
||||
|
||||
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 46 KiB |
BIN
app_page_screeshots/data_page_wide.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
app_page_screeshots/install_page_skinny.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
app_page_screeshots/install_page_wide.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
app_page_screeshots/packages_page_wide.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
app_page_screeshots/propteries_page_skinny.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
app_page_screeshots/remotes_page_wide.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
app_page_screeshots/snapshots_page_wide.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
2
data/icons/arrow-pointing-at-line-down-symbolic.svg
Normal 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 |
2
data/icons/arrow-turn-down-right-symbolic.svg
Normal 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 |
2
data/icons/copy-symbolic.svg
Normal 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 |
2
data/icons/dock-left-symbolic.svg
Normal 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 |
2
data/icons/dot-symbolic.svg
Normal 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 |
2
data/icons/double-ended-arrows-vertical-symbolic.svg
Normal 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 |
2
data/icons/edit-symbolic.svg
Normal 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 |
2
data/icons/error-small-symbolic.svg
Normal 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 |
2
data/icons/file-manager-symbolic.svg
Normal 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 |
7
data/icons/folder-open-symbolic.svg
Normal 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 |
4
data/icons/folder-templates-symbolic.svg
Normal 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 |
4
data/icons/font-x-generic-symbolic.svg
Normal 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 |
2
data/icons/harddisk-symbolic.svg
Normal 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 |
4
data/icons/list-remove-all-symbolic.svg
Normal 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 |
2
data/icons/loupe-large-symbolic.svg
Normal 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 |
2
data/icons/minus-large-symbolic.svg
Normal 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 |
2
data/icons/padlock2-symbolic.svg
Normal 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 |
2
data/icons/pin-small-symbolic.svg
Normal 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 |
2
data/icons/pin-symbolic.svg
Normal 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 |
2
data/icons/server-pick-symbolic.svg
Normal 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 |
2
data/icons/snapshots-alt-symbolic.svg
Normal 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 |
2
data/icons/tag-outline-symbolic.svg
Normal 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 |
2
data/icons/vertical-arrows-long-symbolic.svg
Normal 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 |
4
data/icons/view-list-bullet-symbolic.svg
Normal 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 |
7
data/icons/view-sort-ascending-symbolic.svg
Normal 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 |
7
data/icons/view-sort-descending-symbolic.svg
Normal 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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "."
|
||||
|
||||
@@ -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', ],
|
||||
)
|
||||
|
||||
76
po/POTFILES
@@ -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
|
||||
|
||||
2421
po/warehouse.pot
@@ -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()
|
||||
60
src/change_version_page/change_version_page.blp
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/change_version_page/change_version_page.py
Normal 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)
|
||||
|
||||
114
src/change_version_page/change_version_worker.py
Normal 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
|
||||
535
src/common.py
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
15
src/gtk/attempt_install_dialog.blp
Normal 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");
|
||||
}
|
||||
}
|
||||
76
src/gtk/attempt_install_dialog.py
Normal 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
@@ -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)
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
src/gtk/installation_chooser.blp
Normal 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");
|
||||
}
|
||||
}
|
||||
61
src/gtk/installation_chooser.py
Normal 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())
|
||||
57
src/gtk/loading_status.blp
Normal 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
@@ -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
@@ -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
@@ -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)
|
||||
41
src/install_page/file_install_dialog.blp
Normal 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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/install_page/file_install_dialog.py
Normal 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)
|
||||
107
src/install_page/install_page.blp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/install_page/install_page.py
Normal 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)
|
||||
41
src/install_page/pending_page.blp
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/install_page/pending_page.py
Normal 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
|
||||
49
src/install_page/result_row.blp
Normal 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"]
|
||||
}
|
||||
}
|
||||
71
src/install_page/result_row.py
Normal 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)
|
||||
|
||||
54
src/install_page/results_page.blp
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
142
src/install_page/results_page.py
Normal 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)
|
||||
50
src/install_page/select_page.blp
Normal 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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/install_page/select_page.py
Normal 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
|
||||
220
src/main.py
@@ -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
@@ -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
@@ -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))
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
140
src/package_install_worker.py
Normal 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
|
||||
118
src/packages_page/filters_page.blp
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
196
src/packages_page/filters_page.py
Normal 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())
|
||||
185
src/packages_page/packages_page.blp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
368
src/packages_page/packages_page.py
Normal 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)
|
||||
27
src/packages_page/uninstall_dialog.blp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
;
|
||||
}
|
||||