Add system tray, APK Installer, fix package visibility, new themes/fonts; bump to v1.3.0
- System tray (Linux): pure Go D-Bus StatusNotifierItem, no cgo/libappindicator. Close now offers Minimize to Tray vs Quit instead of always quitting. - New APK Installer module: batch-install from a folder or hand-picked files, live per-file progress, select all/none. - Fix Packages/App Inspector silently hiding packages disabled/uninstalled by Canta/Shizuku (pm list packages needs -u to see them). Added a state filter and per-row Restore/Enable/Disable/Uninstall actions. - 6 new theme palettes (Nord, Tokyo Night, Solarized Dark/Light, Rosé Pine, Everforest - 26 total), custom text colour, 8 bundled display fonts, adjustable font size. - Horizontal sidebar now shrinks label text progressively instead of scrolling when the window narrows; fixed a touchpad stuck-scrollbar-drag bug. - Updated README and fixed stale Go 1.23 -> 1.25 build instructions.
This commit is contained in:
parent
ed97bc2c14
commit
cf3fb26bd2
52 changed files with 2642 additions and 98 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -38,6 +38,10 @@ ATK_*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
|
# Raw font source dump (bundled fonts already live in frontend/src/assets/fonts/)
|
||||||
|
/DotGothic16,Geist_Pixel,Gugi,Orbitron,Playfair_Display,etc/
|
||||||
|
/DotGothic16,Geist_Pixel,Gugi,Orbitron,Playfair_Display,etc.zip
|
||||||
|
|
||||||
# ── PROPRIETARY — Logcat visual map. NEVER publish to GitHub. ──
|
# ── PROPRIETARY — Logcat visual map. NEVER publish to GitHub. ──
|
||||||
# (Builds into the shipped binary as obfuscated JS + native Go; source stays private.)
|
# (Builds into the shipped binary as obfuscated JS + native Go; source stays private.)
|
||||||
/proprietary/
|
/proprietary/
|
||||||
|
|
|
||||||
55
README.md
55
README.md
|
|
@ -143,7 +143,8 @@ sudo apt install scrcpy
|
||||||
| 🖥️ **Screen Mirror** | Live mirror and full control via scrcpy; detachable and recordable |
|
| 🖥️ **Screen Mirror** | Live mirror and full control via scrcpy; detachable and recordable |
|
||||||
| 📊 **Dashboard** | Device info, wireless ADB, reboot controls |
|
| 📊 **Dashboard** | Device info, wireless ADB, reboot controls |
|
||||||
| 📁 **File Explorer** | Browse the device and your computer, push and pull, image viewer |
|
| 📁 **File Explorer** | Browse the device and your computer, push and pull, image viewer |
|
||||||
| 📦 **Package Manager** | Install, uninstall, enable, disable, pull APK, plus **privileged removal of protected system apps without root** |
|
| 📦 **Package Manager** | Install, uninstall, enable, disable, restore, pull APK, plus **privileged removal of protected system apps without root**. Correctly shows packages disabled/uninstalled by other tools (e.g. Canta/Shizuku) instead of hiding them, with a state filter (Enabled/Disabled/Uninstalled) |
|
||||||
|
| 📲 **APK Installer** | Batch-install APKs from a folder or hand-picked files, with live per-file progress and select all/none |
|
||||||
| 🧹 **Debloater** | 5,362 packages across Samsung, Xiaomi, Google, and 11 more OEMs |
|
| 🧹 **Debloater** | 5,362 packages across Samsung, Xiaomi, Google, and 11 more OEMs |
|
||||||
| 💻 **Shell Terminal** | adb shell and host, command library, export session |
|
| 💻 **Shell Terminal** | adb shell and host, command library, export session |
|
||||||
| 📡 **Live Logcat + System Map** | Real-time log streaming, plus a live, interactive map of system behaviour across subsystems |
|
| 📡 **Live Logcat + System Map** | Real-time log streaming, plus a live, interactive map of system behaviour across subsystems |
|
||||||
|
|
@ -158,14 +159,37 @@ sudo apt install scrcpy
|
||||||
| 💿 **GSI Loader** | Boot a Generic System Image via **DSU** (temporary, no unlock/wipe) or a danger-gated **permanent fastboot flash**, with a Treble/ABI/VNDK compatibility pre-check |
|
| 💿 **GSI Loader** | Boot a Generic System Image via **DSU** (temporary, no unlock/wipe) or a danger-gated **permanent fastboot flash**, with a Treble/ABI/VNDK compatibility pre-check |
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Hide any module you don't use from **Settings → Sidebar Features**. Theme
|
> Hide any module you don't use from **Settings → Sidebar Features**. 26 theme
|
||||||
> — Dark, Catppuccin Frappé/Latte/Mocha/Macchiato, Dracula, Gruvbox Material,
|
> palettes — Dark, Catppuccin Frappé/Latte/Mocha/Macchiato, Dracula, Gruvbox
|
||||||
> and a dozen more — plus sidebar position (left, top, bottom) are configurable
|
> Material, Nord, Tokyo Night, Solarized, Rosé Pine, Everforest, and more —
|
||||||
> too.
|
> plus a custom accent/text colour, 8 bundled display fonts, adjustable font
|
||||||
|
> size, and sidebar position (left, top, bottom) are all configurable too. A
|
||||||
|
> system tray icon (Linux) lets ATK keep running in the background instead of
|
||||||
|
> quitting.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ What's new in v1.2.0
|
## ✨ What's new in v1.3.0
|
||||||
|
|
||||||
|
- 🖥️ **System tray** (Linux): closing the window now offers **Minimize to
|
||||||
|
Tray** instead of only Quit, with a tray menu to show/hide the window or
|
||||||
|
quit for good.
|
||||||
|
- 📲 **APK Installer**: a new module to batch-install APKs from a folder or
|
||||||
|
hand-picked files, with live per-file progress and select all/none.
|
||||||
|
- 📦 **Package visibility fix**: Packages and App Inspector were silently
|
||||||
|
hiding any package disabled/uninstalled by tools like Canta or Shizuku
|
||||||
|
(`pm uninstall --user 0` doesn't fully remove it, just hides it from a plain
|
||||||
|
package list). They now show correctly, with a state filter
|
||||||
|
(Enabled/Disabled/Uninstalled) and per-row Restore/Enable/Disable/Uninstall.
|
||||||
|
- 🎨 **6 more theme palettes** — Nord, Tokyo Night, Solarized Dark/Light, Rosé
|
||||||
|
Pine, Everforest (26 total) — plus a custom text colour, 8 bundled display
|
||||||
|
fonts, and an adjustable base font size, all in Settings → Appearance.
|
||||||
|
- 🖱️ **Sidebar polish**: a horizontal (top/bottom) sidebar now shrinks label
|
||||||
|
text progressively as the window narrows instead of showing a scrollbar,
|
||||||
|
and a touchpad-triggered stuck-scrollbar-drag bug is fixed.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Previous release (v1.2.0)</summary>
|
||||||
|
|
||||||
- 💿 **GSI Loader**: boot a Generic System Image via **DSU** (temporary guest OS)
|
- 💿 **GSI Loader**: boot a Generic System Image via **DSU** (temporary guest OS)
|
||||||
or a danger-gated **permanent fastboot flash**, with a built-in compatibility
|
or a danger-gated **permanent fastboot flash**, with a built-in compatibility
|
||||||
|
|
@ -184,17 +208,6 @@ sudo apt install scrcpy
|
||||||
bumped several dependencies with known CVEs (`x/image`, `x/net`, `vite`,
|
bumped several dependencies with known CVEs (`x/image`, `x/net`, `vite`,
|
||||||
`postcss`, `esbuild`).
|
`postcss`, `esbuild`).
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Previous release (v1.1.0)</summary>
|
|
||||||
|
|
||||||
- 📡 **Live System Map**: turn logcat into a live, interactive map of system behaviour across subsystems *(see below)*.
|
|
||||||
- 🧹 **Debloater database grew from 2,157 to 5,362 packages**, with a **privileged uninstall of protected system apps without root** and a one-click **restore**.
|
|
||||||
- 🧰 **Utilities expanded to 631 one-click commands** across 50+ categories.
|
|
||||||
- 🎨 **Themes**: Dark, Catppuccin **Frappé**, and **Latte**. Dismissible safety banners.
|
|
||||||
- 🔎 **APK Audit** exports to **JSON, CSV, SARIF**, with an in-app APK explorer.
|
|
||||||
- 📦 **Smarter package ops**: combined *Disable + Uninstall*, a *disabled* badge, and verify-then-escalate so removals stick.
|
|
||||||
- 🔌 **Offline-capable UI**: fonts are self-hosted, with no runtime CDN fetches.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -351,12 +364,12 @@ Coverage: Samsung · Xiaomi · OnePlus/Oppo · Huawei · Sony · Motorola · LG
|
||||||
**Prerequisites on Ubuntu/Debian**
|
**Prerequisites on Ubuntu/Debian**
|
||||||
```bash
|
```bash
|
||||||
sudo apt install -y build-essential pkg-config libgtk-3-dev \
|
sudo apt install -y build-essential pkg-config libgtk-3-dev \
|
||||||
libwebkit2gtk-4.1-dev libayatana-appindicator3-dev adb fastboot
|
libwebkit2gtk-4.1-dev adb fastboot
|
||||||
|
|
||||||
# Go 1.23
|
# Go 1.25+ (required by go.mod)
|
||||||
wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
|
wget https://go.dev/dl/go1.25.0.linux-amd64.tar.gz
|
||||||
sudo rm -rf /usr/local/go
|
sudo rm -rf /usr/local/go
|
||||||
sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
|
sudo tar -C /usr/local -xzf go1.25.0.linux-amd64.tar.gz
|
||||||
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
|
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
|
||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
|
|
||||||
|
|
|
||||||
64
app.go
64
app.go
|
|
@ -60,6 +60,11 @@ type FileEntry struct {
|
||||||
type PackageInfo struct {
|
type PackageInfo struct {
|
||||||
PackageName string `json:"packageName"`
|
PackageName string `json:"packageName"`
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
|
// IsInstalled is false when the package is present on the system image but
|
||||||
|
// uninstalled for user 0 (e.g. via `pm uninstall --user 0`, which is what
|
||||||
|
// Canta/Shizuku do to "remove" a system app). Such packages are still on
|
||||||
|
// disk and show up in `pm list packages -u`, but not in the plain listing.
|
||||||
|
IsInstalled bool `json:"isInstalled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdbConfig holds user-configurable ADB settings
|
// AdbConfig holds user-configurable ADB settings
|
||||||
|
|
@ -84,6 +89,10 @@ type App struct {
|
||||||
// app-lock "require password for destructive actions" session window
|
// app-lock "require password for destructive actions" session window
|
||||||
dangerMu sync.Mutex
|
dangerMu sync.Mutex
|
||||||
dangerUntil time.Time
|
dangerUntil time.Time
|
||||||
|
|
||||||
|
// window visibility, tracked for the tray's show/hide toggle
|
||||||
|
windowMu sync.Mutex
|
||||||
|
windowShown bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new App instance
|
// NewApp creates a new App instance
|
||||||
|
|
@ -97,8 +106,10 @@ func NewApp() *App {
|
||||||
// Startup is called when the app starts
|
// Startup is called when the app starts
|
||||||
func (a *App) Startup(ctx context.Context) {
|
func (a *App) Startup(ctx context.Context) {
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
|
a.windowShown = true
|
||||||
// Frameless windows can open off-centre on some WMs; centre on launch.
|
// Frameless windows can open off-centre on some WMs; centre on launch.
|
||||||
runtime.WindowCenter(ctx)
|
runtime.WindowCenter(ctx)
|
||||||
|
initTray(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown is called when the app is closing — tidy up spawned child processes
|
// Shutdown is called when the app is closing — tidy up spawned child processes
|
||||||
|
|
@ -111,4 +122,57 @@ func (a *App) Shutdown(ctx context.Context) {
|
||||||
if !detached {
|
if !detached {
|
||||||
a.StopScrcpy()
|
a.StopScrcpy()
|
||||||
}
|
}
|
||||||
|
closeTray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowWindow restores the main window - used by the tray's "Show ATK" /
|
||||||
|
// left-click action and by the frontend's "Minimize to tray" quit dialog.
|
||||||
|
func (a *App) ShowWindow() {
|
||||||
|
a.windowMu.Lock()
|
||||||
|
a.windowShown = true
|
||||||
|
a.windowMu.Unlock()
|
||||||
|
runtime.WindowShow(a.ctx)
|
||||||
|
runtime.WindowUnminimise(a.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HideWindow parks the window (app keeps running, reachable from the tray).
|
||||||
|
func (a *App) HideWindow() {
|
||||||
|
a.windowMu.Lock()
|
||||||
|
a.windowShown = false
|
||||||
|
a.windowMu.Unlock()
|
||||||
|
runtime.WindowHide(a.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToggleWindow shows the window if hidden, hides it if shown - the tray's
|
||||||
|
// left-click / "Show ATK" menu action.
|
||||||
|
func (a *App) ToggleWindow() {
|
||||||
|
a.windowMu.Lock()
|
||||||
|
shown := a.windowShown
|
||||||
|
a.windowMu.Unlock()
|
||||||
|
if shown {
|
||||||
|
a.HideWindow()
|
||||||
|
} else {
|
||||||
|
a.ShowWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrayAvailable reports whether a real system tray icon was registered (e.g.
|
||||||
|
// false on Linux if no StatusNotifierWatcher/AppIndicator host is running).
|
||||||
|
// The frontend uses this to decide whether "Minimize to tray" is even a
|
||||||
|
// sensible option to offer in the close-confirmation dialog.
|
||||||
|
func (a *App) TrayAvailable() bool {
|
||||||
|
return trayAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuitApp fully quits ATK (not just the window) - used by both the tray's
|
||||||
|
// "Quit ATK" menu item and the frontend's close-confirmation dialog.
|
||||||
|
func (a *App) QuitApp() {
|
||||||
|
runtime.Quit(a.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenURL opens a link in the user's default system browser. A bare <a
|
||||||
|
// target="_blank"> isn't reliable inside the embedded webview - this is
|
||||||
|
// Wails' supported way to hand off to the OS.
|
||||||
|
func (a *App) OpenURL(url string) {
|
||||||
|
runtime.BrowserOpenURL(a.ctx, url)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
assets/atk-tray-symbolic.svg
Normal file
1
assets/atk-tray-symbolic.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.28,0 2.5,-0.2 3.68,-0.68l-0.36,-1.9C14.34,19.79 13.19,20 12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8v0.9c0,0.61 -0.6,1.32 -1.5,1.32s-1.5,-0.71 -1.5,-1.32V8h-2v0.68C14.42,8.24 13.51,8 12.5,8C10.02,8 8,10.02 8,12.5s2.02,4.5 4.5,4.5c1.14,0 2.17,-0.42 2.96,-1.11c0.55,0.66 1.42,1.11 2.29,1.11c1.66,0 3.25,-1.34 3.25,-3.32V12C21,6.48 16.52,2 12,2zM12.5,15c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.88,15 12.5,15z" fill="#bebebe"/></svg>
|
||||||
|
After Width: | Height: | Size: 600 B |
BIN
assets/tray_icon.ico
Normal file
BIN
assets/tray_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
assets/tray_icon.png
Normal file
BIN
assets/tray_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 411 B |
BIN
assets/tray_icon_template.png
Normal file
BIN
assets/tray_icon_template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 396 B |
118
backend_apkinstaller.go
Normal file
118
backend_apkinstaller.go
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApkFileInfo describes one APK found in a chosen folder, for the batch
|
||||||
|
// installer's checklist.
|
||||||
|
type ApkFileInfo struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListApksInFolder scans a folder (non-recursive) for .apk files.
|
||||||
|
func (a *App) ListApksInFolder(folderPath string) ([]ApkFileInfo, error) {
|
||||||
|
entries, err := os.ReadDir(folderPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not read folder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var apks []ApkFileInfo
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".apk") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := e.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apks = append(apks, ApkFileInfo{
|
||||||
|
Path: filepath.Join(folderPath, e.Name()),
|
||||||
|
Name: e.Name(),
|
||||||
|
Size: info.Size(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(apks, func(i, j int) bool { return apks[i].Name < apks[j].Name })
|
||||||
|
return apks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatApkFiles builds ApkFileInfo entries for explicitly chosen APK paths
|
||||||
|
// (from the multi-file picker), so manual selection and folder scans feed the
|
||||||
|
// same checklist shape. Unreadable paths are silently skipped.
|
||||||
|
func (a *App) StatApkFiles(paths []string) ([]ApkFileInfo, error) {
|
||||||
|
out := make([]ApkFileInfo, 0, len(paths))
|
||||||
|
for _, p := range paths {
|
||||||
|
info, err := os.Stat(p)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, ApkFileInfo{Path: p, Name: info.Name(), Size: info.Size()})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallApksWithProgress installs each APK in order (adb install -r),
|
||||||
|
// emitting apkinstall:progress events so the UI can show a live per-file
|
||||||
|
// checklist rather than one opaque spinner. Cancellable via CancelOperation.
|
||||||
|
func (a *App) InstallApksWithProgress(paths []string) (string, error) {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return "", fmt.Errorf("no APKs selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := a.beginCancellableOp(0)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
total := len(paths)
|
||||||
|
var ok, fail int
|
||||||
|
var details strings.Builder
|
||||||
|
|
||||||
|
for i, p := range paths {
|
||||||
|
name := baseName(p)
|
||||||
|
// Keyed by full path (not just fileName) - two selected APKs from
|
||||||
|
// different folders can share a filename, and the path is what's
|
||||||
|
// actually unique in the frontend's checklist.
|
||||||
|
runtime.EventsEmit(a.ctx, "apkinstall:progress", map[string]interface{}{
|
||||||
|
"current": i + 1, "total": total, "path": p, "fileName": name, "status": "installing",
|
||||||
|
})
|
||||||
|
|
||||||
|
output, err := a.runCommandContext(ctx, "adb", "install", "-r", p)
|
||||||
|
status, msg := "success", ""
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
status = "failed"
|
||||||
|
msg = err.Error()
|
||||||
|
case strings.Contains(output, "Failure"):
|
||||||
|
status = "failed"
|
||||||
|
msg = strings.TrimSpace(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == "success" {
|
||||||
|
ok++
|
||||||
|
} else {
|
||||||
|
fail++
|
||||||
|
details.WriteString(fmt.Sprintf("• %s: %s\n", name, msg))
|
||||||
|
}
|
||||||
|
runtime.EventsEmit(a.ctx, "apkinstall:progress", map[string]interface{}{
|
||||||
|
"current": i + 1, "total": total, "path": p, "fileName": name, "status": status, "message": msg,
|
||||||
|
})
|
||||||
|
|
||||||
|
if strings.Contains(msg, "cancelled") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runtime.EventsEmit(a.ctx, "apkinstall:done", nil)
|
||||||
|
|
||||||
|
summary := fmt.Sprintf("Installed %d of %d APK(s).", ok, total)
|
||||||
|
if fail > 0 {
|
||||||
|
summary += fmt.Sprintf(" Failed: %d\n%s", fail, details.String())
|
||||||
|
}
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
@ -78,6 +78,33 @@ func (a *App) SelectDirectoryForPull() (string, error) {
|
||||||
return path, err
|
return path, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectApkFolder opens a native directory picker for a folder of APKs to
|
||||||
|
// batch-install.
|
||||||
|
func (a *App) SelectApkFolder() (string, error) {
|
||||||
|
path, err := zenity.SelectFile(
|
||||||
|
zenity.Title("Select a folder of APKs"),
|
||||||
|
zenity.Directory(),
|
||||||
|
)
|
||||||
|
if err == zenity.ErrCanceled {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return path, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectMultipleApkFiles opens a native multi-file picker filtered to APKs.
|
||||||
|
func (a *App) SelectMultipleApkFiles() ([]string, error) {
|
||||||
|
paths, err := zenity.SelectFileMultiple(
|
||||||
|
zenity.Title("Select APKs to install"),
|
||||||
|
zenity.FileFilters{
|
||||||
|
{Name: "APK files", Patterns: []string{"*.apk"}, CaseFold: true},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err == zenity.ErrCanceled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return paths, err
|
||||||
|
}
|
||||||
|
|
||||||
// SelectFileWithFilter opens a file picker with custom file type filters.
|
// SelectFileWithFilter opens a file picker with custom file type filters.
|
||||||
func (a *App) SelectFileWithFilter(title string, patterns []string) (string, error) {
|
func (a *App) SelectFileWithFilter(title string, patterns []string) (string, error) {
|
||||||
filters := []zenity.FileFilter{
|
filters := []zenity.FileFilter{
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
bfb47127747332de1e5119ef153cca76
|
5044e767932c804d924f539d25c7ab46
|
||||||
|
|
@ -5,10 +5,12 @@ import TitleBar from './components/layout/TitleBar'
|
||||||
import DismissibleBanner from './components/DismissibleBanner'
|
import DismissibleBanner from './components/DismissibleBanner'
|
||||||
import LockGate from './components/LockGate'
|
import LockGate from './components/LockGate'
|
||||||
import DangerGate from './components/DangerGate'
|
import DangerGate from './components/DangerGate'
|
||||||
|
import QuitGate from './components/QuitGate'
|
||||||
import ViewDashboard from './components/views/ViewDashboard'
|
import ViewDashboard from './components/views/ViewDashboard'
|
||||||
import ViewFiles from './components/views/ViewFiles'
|
import ViewFiles from './components/views/ViewFiles'
|
||||||
import ViewScreenMirror from './components/views/ViewScreenMirror'
|
import ViewScreenMirror from './components/views/ViewScreenMirror'
|
||||||
import ViewPackages from './components/views/ViewPackages'
|
import ViewPackages from './components/views/ViewPackages'
|
||||||
|
import ViewApkInstaller from './components/views/ViewApkInstaller'
|
||||||
import ViewDebloater from './components/views/ViewDebloater'
|
import ViewDebloater from './components/views/ViewDebloater'
|
||||||
import ViewShell from './components/views/ViewShell'
|
import ViewShell from './components/views/ViewShell'
|
||||||
import ViewLogcat from './components/views/ViewLogcat'
|
import ViewLogcat from './components/views/ViewLogcat'
|
||||||
|
|
@ -55,6 +57,7 @@ export default function App() {
|
||||||
case 'files': return <ViewFiles />
|
case 'files': return <ViewFiles />
|
||||||
case 'mirror': return <ViewScreenMirror />
|
case 'mirror': return <ViewScreenMirror />
|
||||||
case 'packages': return <ViewPackages />
|
case 'packages': return <ViewPackages />
|
||||||
|
case 'apkinstaller': return <ViewApkInstaller />
|
||||||
case 'debloater': return <ViewDebloater />
|
case 'debloater': return <ViewDebloater />
|
||||||
case 'shell': return <ViewShell />
|
case 'shell': return <ViewShell />
|
||||||
case 'logcat': return <ViewLogcat />
|
case 'logcat': return <ViewLogcat />
|
||||||
|
|
@ -88,6 +91,7 @@ export default function App() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-bg-base overflow-hidden rounded-[10px]">
|
<div className="flex flex-col h-full bg-bg-base overflow-hidden rounded-[10px]">
|
||||||
<DangerGate />
|
<DangerGate />
|
||||||
|
<QuitGate />
|
||||||
<TitleBar />
|
<TitleBar />
|
||||||
<div className={`flex-1 flex overflow-hidden ${sidebarPos === 'left' ? 'flex-row' : 'flex-col'}`}>
|
<div className={`flex-1 flex overflow-hidden ${sidebarPos === 'left' ? 'flex-row' : 'flex-col'}`}>
|
||||||
{sidebarPos !== 'bottom' && sidebar}
|
{sidebarPos !== 'bottom' && sidebar}
|
||||||
|
|
|
||||||
BIN
frontend/src/assets/fonts/DotGothic16-Regular.ttf
Normal file
BIN
frontend/src/assets/fonts/DotGothic16-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Gugi-Regular.ttf
Normal file
BIN
frontend/src/assets/fonts/Gugi-Regular.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Orbitron-VariableFont_wght.ttf
Normal file
BIN
frontend/src/assets/fonts/Orbitron-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/PlayfairDisplay-VariableFont_wght.ttf
Normal file
BIN
frontend/src/assets/fonts/PlayfairDisplay-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/PressStart2P-Regular.ttf
Normal file
BIN
frontend/src/assets/fonts/PressStart2P-Regular.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/SpaceGrotesk-VariableFont_wght.ttf
Normal file
BIN
frontend/src/assets/fonts/SpaceGrotesk-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/VT323-Regular.ttf
Normal file
BIN
frontend/src/assets/fonts/VT323-Regular.ttf
Normal file
Binary file not shown.
93
frontend/src/assets/fonts/licenses/DotGothic16-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/DotGothic16-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2020 The DotGothic16 Project Authors (https://github.com/fontworks-fonts/DotGothic16)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
frontend/src/assets/fonts/licenses/Geist_Pixel-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/Geist_Pixel-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2026 The Geist Project Authors (https://github.com/vercel/geist-font)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
frontend/src/assets/fonts/licenses/Gugi-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/Gugi-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright (c) 2017 by TAE System & Typefaces Co.. All rights reserved.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
frontend/src/assets/fonts/licenses/Orbitron-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/Orbitron-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2018 The Orbitron Project Authors (https://github.com/theleagueof/orbitron), with Reserved Font Name: "Orbitron"
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
frontend/src/assets/fonts/licenses/Playfair_Display-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/Playfair_Display-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display"
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
frontend/src/assets/fonts/licenses/Press_Start_2P-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/Press_Start_2P-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
frontend/src/assets/fonts/licenses/Space_Grotesk-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/Space_Grotesk-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
frontend/src/assets/fonts/licenses/VT323-OFL.txt
Normal file
93
frontend/src/assets/fonts/licenses/VT323-OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2011, The VT323 Project Authors (peter.hull@oikoi.com)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
55
frontend/src/components/QuitGate.tsx
Normal file
55
frontend/src/components/QuitGate.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { LogOut, Minus, X } from 'lucide-react'
|
||||||
|
import { HideWindow, QuitApp, TrayAvailable } from '../lib/wails'
|
||||||
|
import { _registerQuitHost } from '../lib/quitgate'
|
||||||
|
|
||||||
|
// Modal shown when the title bar's close button is clicked. Mirrors
|
||||||
|
// Frequency's close dialog: offer Minimize-to-tray (default) alongside Quit,
|
||||||
|
// rather than one silently winning. "Minimize to tray" only shows up if a
|
||||||
|
// tray icon actually registered (TrayAvailable) - otherwise there'd be no way
|
||||||
|
// back to the window.
|
||||||
|
export default function QuitGate() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [trayOk, setTrayOk] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => _registerQuitHost(() => {
|
||||||
|
TrayAvailable().then(setTrayOk).catch(() => setTrayOk(false))
|
||||||
|
setOpen(true)
|
||||||
|
}), [])
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
const close = () => setOpen(false)
|
||||||
|
const minimize = () => { close(); HideWindow() }
|
||||||
|
const quit = () => { close(); QuitApp() }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
onMouseDown={e => { if (e.target === e.currentTarget) close() }}
|
||||||
|
>
|
||||||
|
<div className="card p-5 w-80 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-medium text-text-primary">Close ATK?</p>
|
||||||
|
<button onClick={close} className="btn-ghost text-xs p-1"><X size={14} /></button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-text-muted">
|
||||||
|
{trayOk
|
||||||
|
? 'ATK can keep running in the background and stay reachable from the tray, or quit completely.'
|
||||||
|
: 'No tray icon is available on this system, so closing quits ATK completely.'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button onClick={close} className="btn-ghost text-xs">Cancel</button>
|
||||||
|
{trayOk && (
|
||||||
|
<button onClick={minimize} className="btn-ghost text-xs flex items-center gap-1.5">
|
||||||
|
<Minus size={12} /> Minimize to tray
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button onClick={quit} className="btn-danger text-xs flex items-center gap-1.5">
|
||||||
|
<LogOut size={12} /> Quit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, FolderOpen, Package, Terminal,
|
LayoutDashboard, FolderOpen, Package, PackagePlus, Terminal,
|
||||||
Zap, Wrench, Settings, Shield,
|
Zap, Wrench, Settings, Shield,
|
||||||
ScrollText, Search, Lock, Archive, SlidersHorizontal, ScanSearch, MonitorSmartphone, Rocket, HardDriveDownload
|
ScrollText, Search, Lock, Archive, SlidersHorizontal, ScanSearch, MonitorSmartphone, Rocket, HardDriveDownload
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
@ -22,6 +22,7 @@ const navItems: NavItem[] = [
|
||||||
{ view: 'files', icon: <FolderOpen size={17} />, label: 'Files' },
|
{ view: 'files', icon: <FolderOpen size={17} />, label: 'Files' },
|
||||||
{ view: 'mirror', icon: <MonitorSmartphone size={17} />, label: 'Screen Mirror' },
|
{ view: 'mirror', icon: <MonitorSmartphone size={17} />, label: 'Screen Mirror' },
|
||||||
{ view: 'packages', icon: <Package size={17} />, label: 'Packages' },
|
{ view: 'packages', icon: <Package size={17} />, label: 'Packages' },
|
||||||
|
{ view: 'apkinstaller', icon: <PackagePlus size={17} />, label: 'APK Installer' },
|
||||||
{ view: 'debloater', icon: <Shield size={17} />, label: 'Debloater' },
|
{ view: 'debloater', icon: <Shield size={17} />, label: 'Debloater' },
|
||||||
{ view: 'shell', icon: <Terminal size={17} />, label: 'Shell' },
|
{ view: 'shell', icon: <Terminal size={17} />, label: 'Shell' },
|
||||||
{ view: 'logcat', icon: <ScrollText size={17} />, label: 'Logcat', dividerBefore: true },
|
{ view: 'logcat', icon: <ScrollText size={17} />, label: 'Logcat', dividerBefore: true },
|
||||||
|
|
@ -36,6 +37,10 @@ const navItems: NavItem[] = [
|
||||||
{ view: 'gsiloader', icon: <HardDriveDownload size={17} />, label: 'GSI Loader' },
|
{ view: 'gsiloader', icon: <HardDriveDownload size={17} />, label: 'GSI Loader' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Floor for progressive label shrinking in the horizontal sidebar - below
|
||||||
|
// this, text becomes illegible, so labels hide outright instead.
|
||||||
|
const MIN_LABEL_SCALE = 0.65
|
||||||
|
|
||||||
interface DragProps {
|
interface DragProps {
|
||||||
onDragStart: (e: React.DragEvent) => void
|
onDragStart: (e: React.DragEvent) => void
|
||||||
onDragOver: (e: React.DragEvent) => void
|
onDragOver: (e: React.DragEvent) => void
|
||||||
|
|
@ -51,6 +56,19 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
||||||
const [hidden, setHidden] = useState<string[]>(getHiddenViews())
|
const [hidden, setHidden] = useState<string[]>(getHiddenViews())
|
||||||
useEffect(() => onHiddenViewsChange(setHidden), [])
|
useEffect(() => onHiddenViewsChange(setHidden), [])
|
||||||
|
|
||||||
|
// When horizontal (top/bottom sidebar), labels can make the row wider than
|
||||||
|
// the window - rather than forcing a horizontal scrollbar, shrink label text
|
||||||
|
// progressively as space gets tight (labelScale 1 -> MIN_LABEL_SCALE), and
|
||||||
|
// only hide labels outright once even minimum-size text wouldn't fit.
|
||||||
|
// `measureRef` is an offscreen clone always rendered WITH full-size labels
|
||||||
|
// at natural width; comparing its scrollWidth against the real nav's
|
||||||
|
// clientWidth (which itself doesn't change as labelScale changes) avoids
|
||||||
|
// the flip-flop you'd get measuring the visible, already-shrunk row.
|
||||||
|
const navRef = useRef<HTMLElement>(null)
|
||||||
|
const measureRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [labelScale, setLabelScale] = useState(1)
|
||||||
|
const effectiveShowLabels = showLabels && (!horizontal || labelScale > 0)
|
||||||
|
|
||||||
// Drag-to-reorder (dock style). Saved order first, then any new defaults.
|
// Drag-to-reorder (dock style). Saved order first, then any new defaults.
|
||||||
const [order, setOrder] = useState<string[]>(getNavOrder())
|
const [order, setOrder] = useState<string[]>(getNavOrder())
|
||||||
const dragRef = useRef<string | null>(null)
|
const dragRef = useRef<string | null>(null)
|
||||||
|
|
@ -70,6 +88,21 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
||||||
|
|
||||||
const visibleItems = ordered.filter(i => !hidden.includes(i.view))
|
const visibleItems = ordered.filter(i => !hidden.includes(i.view))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!horizontal || !showLabels) { setLabelScale(1); return }
|
||||||
|
const nav = navRef.current
|
||||||
|
const measure = measureRef.current
|
||||||
|
if (!nav || !measure) return
|
||||||
|
const check = () => {
|
||||||
|
const ratio = measure.scrollWidth > 0 ? nav.clientWidth / measure.scrollWidth : 1
|
||||||
|
setLabelScale(ratio >= 1 ? 1 : ratio >= MIN_LABEL_SCALE ? ratio : 0)
|
||||||
|
}
|
||||||
|
check()
|
||||||
|
const ro = new ResizeObserver(check)
|
||||||
|
ro.observe(nav)
|
||||||
|
return () => ro.disconnect()
|
||||||
|
}, [horizontal, showLabels, visibleItems])
|
||||||
|
|
||||||
const handleDrop = (target: string) => {
|
const handleDrop = (target: string) => {
|
||||||
const from = dragRef.current
|
const from = dragRef.current
|
||||||
dragRef.current = null
|
dragRef.current = null
|
||||||
|
|
@ -89,8 +122,8 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
||||||
position === 'left' ? 'border-r' : position === 'top' ? 'border-b' : 'border-t'
|
position === 'left' ? 'border-r' : position === 'top' ? 'border-b' : 'border-t'
|
||||||
|
|
||||||
const asideCls = horizontal
|
const asideCls = horizontal
|
||||||
? `${showLabels ? 'h-[68px]' : 'h-[52px]'} w-full flex flex-row items-center bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
? `${effectiveShowLabels ? 'h-[68px]' : 'h-[52px]'} w-full flex flex-row items-center bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
||||||
: `${showLabels ? 'w-[84px]' : 'w-[52px]'} flex flex-col bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
: `${effectiveShowLabels ? 'w-[84px]' : 'w-[52px]'} flex flex-col bg-bg-surface ${edgeBorder} border-bg-border shrink-0`
|
||||||
|
|
||||||
const navCls = horizontal
|
const navCls = horizontal
|
||||||
? 'flex-1 flex flex-row items-center justify-center gap-0.5 px-1 overflow-x-auto'
|
? 'flex-1 flex flex-row items-center justify-center gap-0.5 px-1 overflow-x-auto'
|
||||||
|
|
@ -102,13 +135,19 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
||||||
? 'px-1 h-full flex items-center border-l border-bg-border shrink-0'
|
? 'px-1 h-full flex items-center border-l border-bg-border shrink-0'
|
||||||
: 'p-1 pb-1.5 border-t border-bg-border shrink-0'
|
: 'p-1 pb-1.5 border-t border-bg-border shrink-0'
|
||||||
|
|
||||||
const btnSizing = !showLabels
|
const btnSizing = !effectiveShowLabels
|
||||||
? 'w-8 h-8'
|
? 'w-8 h-8'
|
||||||
: horizontal
|
: horizontal
|
||||||
? 'flex-col gap-1 px-2 py-1.5 min-w-[3.25rem] h-full justify-center'
|
? 'flex-col gap-1 px-2 py-1.5 h-full justify-center'
|
||||||
: 'flex-col gap-1 px-1 py-1.5 w-full'
|
: 'flex-col gap-1 px-1 py-1.5 w-full'
|
||||||
|
|
||||||
const labelCls = `text-[10px] leading-tight text-center ${horizontal ? 'whitespace-nowrap' : ''}`
|
const labelCls = `leading-tight text-center ${horizontal ? 'whitespace-nowrap' : ''}`
|
||||||
|
|
||||||
|
// Horizontal-only: button width and label font-size both track labelScale,
|
||||||
|
// so shrinking text actually reclaims row space instead of just looking smaller.
|
||||||
|
const btnStyle: React.CSSProperties | undefined =
|
||||||
|
horizontal && effectiveShowLabels ? { minWidth: `${3.25 * labelScale}rem` } : undefined
|
||||||
|
const labelStyle: React.CSSProperties = { fontSize: `${10 * labelScale}px` }
|
||||||
|
|
||||||
const renderButton = (view: View | 'settings', icon: React.ReactNode, label: string, drag?: DragProps) => {
|
const renderButton = (view: View | 'settings', icon: React.ReactNode, label: string, drag?: DragProps) => {
|
||||||
const active = activeView === view
|
const active = activeView === view
|
||||||
|
|
@ -122,6 +161,7 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
||||||
onDragEnd={drag?.onDragEnd}
|
onDragEnd={drag?.onDragEnd}
|
||||||
onClick={() => onViewChange(view as View)}
|
onClick={() => onViewChange(view as View)}
|
||||||
title={label}
|
title={label}
|
||||||
|
style={btnStyle}
|
||||||
className={`
|
className={`
|
||||||
flex items-center justify-center rounded transition-all duration-150 relative
|
flex items-center justify-center rounded transition-all duration-150 relative
|
||||||
${btnSizing}
|
${btnSizing}
|
||||||
|
|
@ -134,7 +174,7 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{showLabels && <span className={labelCls}>{label}</span>}
|
{effectiveShowLabels && <span className={labelCls} style={labelStyle}>{label}</span>}
|
||||||
{active && (
|
{active && (
|
||||||
horizontal
|
horizontal
|
||||||
? <span className="absolute bottom-0 left-1/2 -translate-x-1/2 h-0.5 w-5 bg-accent-green rounded-t" />
|
? <span className="absolute bottom-0 left-1/2 -translate-x-1/2 h-0.5 w-5 bg-accent-green rounded-t" />
|
||||||
|
|
@ -146,7 +186,25 @@ export default function Sidebar({ activeView, onViewChange, position, showLabels
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={asideCls}>
|
<aside className={asideCls}>
|
||||||
<nav className={navCls}>
|
{horizontal && showLabels && (
|
||||||
|
<div ref={measureRef} aria-hidden="true" className="fixed -top-[9999px] left-0 flex flex-row items-center gap-0.5 px-1 pointer-events-none">
|
||||||
|
{visibleItems.map(({ view, icon, label, dividerBefore }, idx) => (
|
||||||
|
<div key={view} className="flex items-center">
|
||||||
|
{dividerBefore && idx > 0 && <div className={dividerCls} />}
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1 px-2 py-1.5 min-w-[3.25rem] h-[68px]">
|
||||||
|
{icon}
|
||||||
|
<span className="text-[10px] leading-tight text-center whitespace-nowrap">{label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="w-px h-7 bg-bg-border mx-1" />
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1 px-2 py-1.5 min-w-[3.25rem] h-[68px]">
|
||||||
|
<Settings size={17} />
|
||||||
|
<span className="text-[10px] leading-tight text-center whitespace-nowrap">Settings</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<nav ref={navRef} className={navCls}>
|
||||||
{visibleItems.map(({ view, icon, label, dividerBefore }, idx) => (
|
{visibleItems.map(({ view, icon, label, dividerBefore }, idx) => (
|
||||||
<div key={view} className={horizontal ? 'flex items-center' : undefined}>
|
<div key={view} className={horizontal ? 'flex items-center' : undefined}>
|
||||||
{dividerBefore && idx > 0 && <div className={dividerCls} />}
|
{dividerBefore && idx > 0 && <div className={dividerCls} />}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
// the drag region via Wails' `--wails-draggable:drag` CSS hint, plus macOS-style
|
// the drag region via Wails' `--wails-draggable:drag` CSS hint, plus macOS-style
|
||||||
// traffic-light controls tinted in Catppuccin pastels. No app name by design.
|
// traffic-light controls tinted in Catppuccin pastels. No app name by design.
|
||||||
|
|
||||||
|
import { requestQuitConfirm } from '../../lib/quitgate'
|
||||||
|
|
||||||
// Runtime is injected by Wails on window['runtime'] (same access pattern as
|
// Runtime is injected by Wails on window['runtime'] (same access pattern as
|
||||||
// ViewLogcat.tsx); guarded with ?. so a browser dev session won't crash.
|
// ViewLogcat.tsx); guarded with ?. so a browser dev session won't crash.
|
||||||
const rt = () => (window as any)['runtime']
|
const rt = () => (window as any)['runtime']
|
||||||
|
|
@ -31,7 +33,7 @@ export default function TitleBar() {
|
||||||
<div className="flex items-center gap-2 ml-auto" style={{ '--wails-draggable': 'no-drag' } as React.CSSProperties}>
|
<div className="flex items-center gap-2 ml-auto" style={{ '--wails-draggable': 'no-drag' } as React.CSSProperties}>
|
||||||
<TrafficLight color="#e5c890" hover="hover:brightness-110" title="Minimise" onClick={() => rt()?.WindowMinimise?.()} />
|
<TrafficLight color="#e5c890" hover="hover:brightness-110" title="Minimise" onClick={() => rt()?.WindowMinimise?.()} />
|
||||||
<TrafficLight color="#a6d189" hover="hover:brightness-110" title="Maximise" onClick={() => rt()?.WindowToggleMaximise?.()} />
|
<TrafficLight color="#a6d189" hover="hover:brightness-110" title="Maximise" onClick={() => rt()?.WindowToggleMaximise?.()} />
|
||||||
<TrafficLight color="#e78284" hover="hover:brightness-110" title="Close" onClick={() => rt()?.Quit?.()} />
|
<TrafficLight color="#e78284" hover="hover:brightness-110" title="Close" onClick={requestQuitConfirm} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
251
frontend/src/components/views/ViewApkInstaller.tsx
Normal file
251
frontend/src/components/views/ViewApkInstaller.tsx
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||||
|
import {
|
||||||
|
PackagePlus, FolderOpen, FilePlus, Trash2, Search, Download,
|
||||||
|
CheckCircle2, XCircle, Loader2, X, Ban
|
||||||
|
} from 'lucide-react'
|
||||||
|
import {
|
||||||
|
SelectApkFolder, SelectMultipleApkFiles, ListApksInFolder, StatApkFiles,
|
||||||
|
InstallApksWithProgress, CancelOperation
|
||||||
|
} from '../../lib/wails'
|
||||||
|
import { notify } from '../../lib/notify'
|
||||||
|
import type { ApkFileInfo } from '../../lib/types'
|
||||||
|
|
||||||
|
const rt = () => (window as any)['runtime']
|
||||||
|
|
||||||
|
type RowStatus = 'idle' | 'pending' | 'installing' | 'success' | 'failed'
|
||||||
|
interface Progress { status: RowStatus; message?: string }
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`
|
||||||
|
if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} KB`
|
||||||
|
return `${bytes} B`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ViewApkInstaller() {
|
||||||
|
const [apks, setApks] = useState<ApkFileInfo[]>([])
|
||||||
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||||
|
const [progress, setProgress] = useState<Record<string, Progress>>({})
|
||||||
|
const [installing, setInstalling] = useState(false)
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
// Live per-file install status, driven by backend apkinstall:* events.
|
||||||
|
useEffect(() => {
|
||||||
|
const onProgress = (p: { current: number; total: number; path: string; status: RowStatus; message?: string }) => {
|
||||||
|
setProgress(prev => ({ ...prev, [p.path]: { status: p.status, message: p.message } }))
|
||||||
|
}
|
||||||
|
const onDone = () => setInstalling(false)
|
||||||
|
const off1 = rt()?.EventsOn?.('apkinstall:progress', onProgress)
|
||||||
|
const off2 = rt()?.EventsOn?.('apkinstall:done', onDone)
|
||||||
|
return () => { off1?.(); off2?.() }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const mergeApks = useCallback((incoming: ApkFileInfo[]) => {
|
||||||
|
if (incoming.length === 0) return
|
||||||
|
setApks(prev => {
|
||||||
|
const byPath = new Map(prev.map(a => [a.path, a]))
|
||||||
|
for (const a of incoming) byPath.set(a.path, a)
|
||||||
|
return [...byPath.values()].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
})
|
||||||
|
setSelected(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
for (const a of incoming) next.add(a.path)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const addFolder = async () => {
|
||||||
|
try {
|
||||||
|
const folder = await SelectApkFolder()
|
||||||
|
if (!folder) return
|
||||||
|
const found = await ListApksInFolder(folder)
|
||||||
|
if (!found || found.length === 0) { notify.error('No APKs found in that folder'); return }
|
||||||
|
mergeApks(found)
|
||||||
|
} catch (e: any) {
|
||||||
|
notify.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFiles = async () => {
|
||||||
|
try {
|
||||||
|
const paths = await SelectMultipleApkFiles()
|
||||||
|
if (!paths || paths.length === 0) return
|
||||||
|
const found = await StatApkFiles(paths)
|
||||||
|
mergeApks(found || [])
|
||||||
|
} catch (e: any) {
|
||||||
|
notify.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeOne = (path: string) => {
|
||||||
|
setApks(prev => prev.filter(a => a.path !== path))
|
||||||
|
setSelected(prev => { const next = new Set(prev); next.delete(path); return next })
|
||||||
|
setProgress(prev => { const { [path]: _drop, ...rest } = prev; return rest })
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAll = () => { setApks([]); setSelected(new Set()); setProgress({}) }
|
||||||
|
|
||||||
|
const filtered = useMemo(() =>
|
||||||
|
apks.filter(a => a.name.toLowerCase().includes(search.toLowerCase())),
|
||||||
|
[apks, search]
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleSelect = (path: string) => setSelected(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.has(path) ? next.delete(path) : next.add(path)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
if (selected.size === filtered.length) {
|
||||||
|
setSelected(new Set())
|
||||||
|
} else {
|
||||||
|
setSelected(new Set(filtered.map(a => a.path)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const install = async () => {
|
||||||
|
if (selected.size === 0) { notify.error('Select at least one APK'); return }
|
||||||
|
const targets = apks.filter(a => selected.has(a.path))
|
||||||
|
setInstalling(true)
|
||||||
|
const reset: Record<string, Progress> = {}
|
||||||
|
for (const t of targets) reset[t.path] = { status: 'pending' }
|
||||||
|
setProgress(prev => ({ ...prev, ...reset }))
|
||||||
|
try {
|
||||||
|
const summary = await InstallApksWithProgress(targets.map(t => t.path))
|
||||||
|
notify.success(summary)
|
||||||
|
} catch (e: any) {
|
||||||
|
notify.error(e)
|
||||||
|
} finally {
|
||||||
|
setInstalling(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = async () => {
|
||||||
|
try { await CancelOperation() } catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusIcon = (status: RowStatus | undefined) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'installing': return <Loader2 size={13} className="animate-spin text-accent-green" />
|
||||||
|
case 'success': return <CheckCircle2 size={13} className="text-accent-green" />
|
||||||
|
case 'failed': return <XCircle size={13} className="text-danger" />
|
||||||
|
case 'pending': return <span className="w-[13px] h-[13px] rounded-full border border-text-muted inline-block" />
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="border-b border-bg-border px-4 py-2 flex items-center gap-2 shrink-0 flex-wrap">
|
||||||
|
<PackagePlus size={14} className="text-accent-green shrink-0" />
|
||||||
|
<span className="text-xs text-text-secondary">APK Installer</span>
|
||||||
|
|
||||||
|
<button onClick={addFolder} disabled={installing} className="btn-ghost text-xs">
|
||||||
|
<FolderOpen size={13} /> Add Folder
|
||||||
|
</button>
|
||||||
|
<button onClick={addFiles} disabled={installing} className="btn-ghost text-xs">
|
||||||
|
<FilePlus size={13} /> Add APKs
|
||||||
|
</button>
|
||||||
|
{apks.length > 0 && (
|
||||||
|
<button onClick={clearAll} disabled={installing} className="btn-ghost text-xs">
|
||||||
|
<Trash2 size={13} /> Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative flex-1 min-w-[160px]">
|
||||||
|
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
||||||
|
<input
|
||||||
|
className="input pl-8 text-xs"
|
||||||
|
placeholder="Filter list..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-px h-5 bg-bg-border" />
|
||||||
|
|
||||||
|
{installing ? (
|
||||||
|
<button onClick={cancel} className="btn-danger text-xs">
|
||||||
|
<Ban size={13} /> Cancel
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button onClick={install} disabled={selected.size === 0} className="btn-primary text-xs">
|
||||||
|
<Download size={13} /> Install Selected ({selected.size})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List header */}
|
||||||
|
{apks.length > 0 && (
|
||||||
|
<div className="grid grid-cols-[24px_20px_1fr_80px_60px] gap-2 px-4 py-1.5 border-b border-bg-border text-text-muted text-xs shrink-0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filtered.length > 0 && selected.size === filtered.length}
|
||||||
|
onChange={selectAll}
|
||||||
|
disabled={installing}
|
||||||
|
className="accent-accent-green"
|
||||||
|
/>
|
||||||
|
<span />
|
||||||
|
<span>APK</span>
|
||||||
|
<span>Size</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
{apks.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-3 text-text-muted">
|
||||||
|
<PackagePlus size={28} className="opacity-30" />
|
||||||
|
<p className="text-sm">Add a folder of APKs, or pick files individually</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{filtered.map(a => {
|
||||||
|
const prog = progress[a.path]
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={a.path}
|
||||||
|
className={`
|
||||||
|
grid grid-cols-[24px_20px_1fr_80px_60px] gap-2 px-4 py-2
|
||||||
|
border-b border-bg-border/50 items-center text-xs
|
||||||
|
hover:bg-bg-raised transition-colors
|
||||||
|
${selected.has(a.path) ? 'bg-accent-green/5' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.has(a.path)}
|
||||||
|
onChange={() => toggleSelect(a.path)}
|
||||||
|
disabled={installing}
|
||||||
|
className="accent-accent-green"
|
||||||
|
/>
|
||||||
|
<span title={prog?.message}>{statusIcon(prog?.status)}</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="mono text-text-secondary truncate">{a.name}</p>
|
||||||
|
{prog?.status === 'failed' && prog.message && (
|
||||||
|
<p className="text-danger text-[11px] truncate" title={prog.message}>{prog.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-text-muted">{formatSize(a.size)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeOne(a.path)}
|
||||||
|
disabled={installing}
|
||||||
|
className="btn-ghost text-xs py-0.5 px-1.5 justify-self-start"
|
||||||
|
title="Remove from list"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status bar */}
|
||||||
|
<div className="border-t border-bg-border px-4 py-1.5 flex items-center justify-between text-xs text-text-muted shrink-0">
|
||||||
|
<span>{filtered.length} APK(s){search ? ` matching "${search}"` : ''}</span>
|
||||||
|
{selected.size > 0 && <span>{selected.size} selected</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,11 @@ const GRADE_COLOR: Record<string, string> = {
|
||||||
A: 'text-accent-green', B: 'text-accent-green', C: 'text-warn', D: 'text-warn', F: 'text-danger',
|
A: 'text-accent-green', B: 'text-accent-green', C: 'text-warn', D: 'text-warn', F: 'text-danger',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StateFilter = 'all' | 'enabled' | 'disabled' | 'uninstalled'
|
||||||
|
|
||||||
export default function ViewAppInspect() {
|
export default function ViewAppInspect() {
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [stateFilter, setStateFilter] = useState<StateFilter>('all')
|
||||||
const [packages, setPackages] = useState<PackageInfo[]>([])
|
const [packages, setPackages] = useState<PackageInfo[]>([])
|
||||||
const [pkgsLoaded, setPkgsLoaded] = useState(false)
|
const [pkgsLoaded, setPkgsLoaded] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
@ -101,9 +104,16 @@ export default function ViewAppInspect() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = packages.filter(p =>
|
const filtered = packages
|
||||||
p.packageName.toLowerCase().includes(search.toLowerCase())
|
.filter(p => p.packageName.toLowerCase().includes(search.toLowerCase()))
|
||||||
)
|
.filter(p => {
|
||||||
|
switch (stateFilter) {
|
||||||
|
case 'enabled': return p.isInstalled && p.isEnabled
|
||||||
|
case 'disabled': return p.isInstalled && !p.isEnabled
|
||||||
|
case 'uninstalled': return !p.isInstalled
|
||||||
|
default: return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'overview', label: 'Overview', icon: <Package size={12} /> },
|
{ id: 'overview', label: 'Overview', icon: <Package size={12} /> },
|
||||||
|
|
@ -135,6 +145,17 @@ export default function ViewAppInspect() {
|
||||||
<button onClick={() => inspect(search)} disabled={!search || loading} className="btn-primary text-xs w-full justify-center">
|
<button onClick={() => inspect(search)} disabled={!search || loading} className="btn-primary text-xs w-full justify-center">
|
||||||
{loading ? 'Inspecting...' : 'Inspect'}
|
{loading ? 'Inspecting...' : 'Inspect'}
|
||||||
</button>
|
</button>
|
||||||
|
<select
|
||||||
|
className="input text-xs w-full"
|
||||||
|
value={stateFilter}
|
||||||
|
onChange={e => setStateFilter(e.target.value as StateFilter)}
|
||||||
|
title="Filter by state"
|
||||||
|
>
|
||||||
|
<option value="all">All states</option>
|
||||||
|
<option value="enabled">Enabled</option>
|
||||||
|
<option value="disabled">Disabled</option>
|
||||||
|
<option value="uninstalled">Uninstalled</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
|
|
@ -145,7 +166,9 @@ export default function ViewAppInspect() {
|
||||||
className="w-full text-left px-3 py-2 text-xs text-text-secondary hover:bg-bg-raised hover:text-text-primary transition-colors border-b border-bg-border/30"
|
className="w-full text-left px-3 py-2 text-xs text-text-secondary hover:bg-bg-raised hover:text-text-primary transition-colors border-b border-bg-border/30"
|
||||||
>
|
>
|
||||||
<p className="truncate mono">{p.packageName}</p>
|
<p className="truncate mono">{p.packageName}</p>
|
||||||
<p className={p.isEnabled ? 'text-accent-green' : 'text-danger'}>{p.isEnabled ? 'enabled' : 'disabled'}</p>
|
<p className={!p.isInstalled ? 'text-text-muted' : p.isEnabled ? 'text-accent-green' : 'text-danger'}>
|
||||||
|
{!p.isInstalled ? 'uninstalled' : p.isEnabled ? 'enabled' : 'disabled'}
|
||||||
|
</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{!pkgsLoaded && (
|
{!pkgsLoaded && (
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,14 @@ interface Row {
|
||||||
category: string
|
category: string
|
||||||
deps?: string[]
|
deps?: string[]
|
||||||
neededBy?: string[]
|
neededBy?: string[]
|
||||||
isInstalled: boolean
|
// Physically present on the system image (pm list packages -u), regardless
|
||||||
|
// of per-user install state. False only for packages that have never been
|
||||||
|
// seen on this device at all (UAD-database-only entries).
|
||||||
|
onDevice: boolean
|
||||||
|
// onDevice but uninstalled for user 0 - e.g. via Canta/Shizuku, which runs
|
||||||
|
// `pm uninstall --user 0`. The APK is still on disk; it just isn't active
|
||||||
|
// for this user until restored.
|
||||||
|
isUninstalledForUser: boolean
|
||||||
isDisabled: boolean
|
isDisabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +63,8 @@ export default function ViewDebloater() {
|
||||||
const [mfrFilter, setMfrFilter] = useState('all')
|
const [mfrFilter, setMfrFilter] = useState('all')
|
||||||
const [openCats, setOpenCats] = useState<Set<string>>(new Set())
|
const [openCats, setOpenCats] = useState<Set<string>>(new Set())
|
||||||
const [operating, setOperating] = useState(false)
|
const [operating, setOperating] = useState(false)
|
||||||
const [stateFilter, setStateFilter] = useState<'installed' | 'enabled' | 'disabled' | 'notinstalled' | 'all'>('installed')
|
const [stateFilter, setStateFilter] = useState<'installed' | 'enabled' | 'disabled' | 'uninstalled' | 'notinstalled' | 'all'>('installed')
|
||||||
|
const [uninstalledForUser, setUninstalledForUser] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
// pkg -> UAD database entry (with its category). Built once; first match wins.
|
// pkg -> UAD database entry (with its category). Built once; first match wins.
|
||||||
const dbIndex = useMemo(() => {
|
const dbIndex = useMemo(() => {
|
||||||
|
|
@ -67,7 +75,7 @@ export default function ViewDebloater() {
|
||||||
m.set(p.pkg, {
|
m.set(p.pkg, {
|
||||||
pkg: p.pkg, label: p.label, description: p.description, safety: p.safety,
|
pkg: p.pkg, label: p.label, description: p.description, safety: p.safety,
|
||||||
category: cat.name, deps: p.deps, neededBy: p.neededBy,
|
category: cat.name, deps: p.deps, neededBy: p.neededBy,
|
||||||
isInstalled: false, isDisabled: false,
|
onDevice: false, isUninstalledForUser: false, isDisabled: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,12 +87,16 @@ export default function ViewDebloater() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setInstalled(new Set())
|
setInstalled(new Set())
|
||||||
setDisabled(new Set())
|
setDisabled(new Set())
|
||||||
|
setUninstalledForUser(new Set())
|
||||||
setSelected(new Set())
|
setSelected(new Set())
|
||||||
try {
|
try {
|
||||||
|
// 'all' now includes packages uninstalled-for-user (pm list packages -u) -
|
||||||
|
// still physically on the device, just not active for user 0.
|
||||||
const pkgs = await ListPackages('all')
|
const pkgs = await ListPackages('all')
|
||||||
const names = new Set<string>((pkgs || []).map((p: PackageInfo) => p.packageName))
|
const names = new Set<string>((pkgs || []).map((p: PackageInfo) => p.packageName))
|
||||||
setInstalled(names)
|
setInstalled(names)
|
||||||
setDisabled(new Set<string>((pkgs || []).filter((p: PackageInfo) => !p.isEnabled).map((p: PackageInfo) => p.packageName)))
|
setDisabled(new Set<string>((pkgs || []).filter((p: PackageInfo) => p.isInstalled && !p.isEnabled).map((p: PackageInfo) => p.packageName)))
|
||||||
|
setUninstalledForUser(new Set<string>((pkgs || []).filter((p: PackageInfo) => !p.isInstalled).map((p: PackageInfo) => p.packageName)))
|
||||||
// Auto-open every category that has at least one package on the device.
|
// Auto-open every category that has at least one package on the device.
|
||||||
const withInstalled = new Set<string>()
|
const withInstalled = new Set<string>()
|
||||||
names.forEach(name => withInstalled.add(dbIndex.get(name)?.category ?? UNCATEGORIZED))
|
names.forEach(name => withInstalled.add(dbIndex.get(name)?.category ?? UNCATEGORIZED))
|
||||||
|
|
@ -107,22 +119,23 @@ export default function ViewDebloater() {
|
||||||
const rows: Row[] = []
|
const rows: Row[] = []
|
||||||
installed.forEach(name => {
|
installed.forEach(name => {
|
||||||
const e = dbIndex.get(name)
|
const e = dbIndex.get(name)
|
||||||
|
const isUninstalledForUser = uninstalledForUser.has(name)
|
||||||
if (e) {
|
if (e) {
|
||||||
rows.push({ ...e, isInstalled: true, isDisabled: disabled.has(name) })
|
rows.push({ ...e, onDevice: true, isUninstalledForUser, isDisabled: disabled.has(name) })
|
||||||
} else {
|
} else {
|
||||||
rows.push({
|
rows.push({
|
||||||
pkg: name, label: shortLabel(name),
|
pkg: name, label: shortLabel(name),
|
||||||
description: 'Not in the debloat database — likely an OEM, carrier, or region-specific package. Safety unknown; research before removing.',
|
description: 'Not in the debloat database — likely an OEM, carrier, or region-specific package. Safety unknown; research before removing.',
|
||||||
safety: 'unknown', category: UNCATEGORIZED,
|
safety: 'unknown', category: UNCATEGORIZED,
|
||||||
isInstalled: true, isDisabled: disabled.has(name),
|
onDevice: true, isUninstalledForUser, isDisabled: disabled.has(name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
dbIndex.forEach((e, pkg) => {
|
dbIndex.forEach((e, pkg) => {
|
||||||
if (!installed.has(pkg)) rows.push({ ...e, isInstalled: false, isDisabled: false })
|
if (!installed.has(pkg)) rows.push({ ...e, onDevice: false, isUninstalledForUser: false, isDisabled: false })
|
||||||
})
|
})
|
||||||
return rows
|
return rows
|
||||||
}, [installed, disabled, dbIndex])
|
}, [installed, disabled, uninstalledForUser, dbIndex])
|
||||||
|
|
||||||
const visibleCategories = useMemo(() => {
|
const visibleCategories = useMemo(() => {
|
||||||
const q = search.toLowerCase()
|
const q = search.toLowerCase()
|
||||||
|
|
@ -131,10 +144,11 @@ export default function ViewDebloater() {
|
||||||
if (mfrFilter !== 'all' && r.category !== mfrFilter) continue
|
if (mfrFilter !== 'all' && r.category !== mfrFilter) continue
|
||||||
if (safetyFilter !== 'all' && r.safety !== safetyFilter) continue
|
if (safetyFilter !== 'all' && r.safety !== safetyFilter) continue
|
||||||
switch (stateFilter) {
|
switch (stateFilter) {
|
||||||
case 'installed': if (!r.isInstalled) continue; break
|
case 'installed': if (!r.onDevice) continue; break
|
||||||
case 'enabled': if (!r.isInstalled || r.isDisabled) continue; break
|
case 'enabled': if (!r.onDevice || r.isUninstalledForUser || r.isDisabled) continue; break
|
||||||
case 'disabled': if (!r.isDisabled) continue; break
|
case 'disabled': if (!r.isDisabled) continue; break
|
||||||
case 'notinstalled': if (r.isInstalled) continue; break
|
case 'uninstalled': if (!r.isUninstalledForUser) continue; break
|
||||||
|
case 'notinstalled': if (r.onDevice) continue; break
|
||||||
// 'all' → no state restriction
|
// 'all' → no state restriction
|
||||||
}
|
}
|
||||||
if (q && !(r.pkg.toLowerCase().includes(q) || r.label.toLowerCase().includes(q) || r.description.toLowerCase().includes(q))) continue
|
if (q && !(r.pkg.toLowerCase().includes(q) || r.label.toLowerCase().includes(q) || r.description.toLowerCase().includes(q))) continue
|
||||||
|
|
@ -243,7 +257,8 @@ export default function ViewDebloater() {
|
||||||
<option value="installed">On device</option>
|
<option value="installed">On device</option>
|
||||||
<option value="enabled">Enabled</option>
|
<option value="enabled">Enabled</option>
|
||||||
<option value="disabled">Disabled</option>
|
<option value="disabled">Disabled</option>
|
||||||
<option value="notinstalled">Not installed</option>
|
<option value="uninstalled">Uninstalled (this user)</option>
|
||||||
|
<option value="notinstalled">Not on device</option>
|
||||||
<option value="all">All</option>
|
<option value="all">All</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
@ -332,7 +347,7 @@ export default function ViewDebloater() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && visibleCategories.map(cat => {
|
{!loading && visibleCategories.map(cat => {
|
||||||
const installedCount = cat.packages.filter(p => p.isInstalled).length
|
const installedCount = cat.packages.filter(p => p.onDevice).length
|
||||||
const isOpen = openCats.has(cat.name)
|
const isOpen = openCats.has(cat.name)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -358,7 +373,8 @@ export default function ViewDebloater() {
|
||||||
|
|
||||||
{/* Packages */}
|
{/* Packages */}
|
||||||
{isOpen && cat.packages.map(p => {
|
{isOpen && cat.packages.map(p => {
|
||||||
const isInst = p.isInstalled
|
const isInst = p.onDevice
|
||||||
|
const isUninstalledForUser = p.isUninstalledForUser
|
||||||
const isDisabled = p.isDisabled
|
const isDisabled = p.isDisabled
|
||||||
const isSel = selected.has(p.pkg)
|
const isSel = selected.has(p.pkg)
|
||||||
const safety = SAFETY_CONFIG[p.safety]
|
const safety = SAFETY_CONFIG[p.safety]
|
||||||
|
|
@ -388,7 +404,8 @@ export default function ViewDebloater() {
|
||||||
{safety.icon} {safety.label}
|
{safety.icon} {safety.label}
|
||||||
</span>
|
</span>
|
||||||
{!isInst && <span className="badge-gray text-xs">not on device</span>}
|
{!isInst && <span className="badge-gray text-xs">not on device</span>}
|
||||||
{isInst && isDisabled && <span className="badge-yellow text-xs">disabled</span>}
|
{isInst && isUninstalledForUser && <span className="badge-gray text-xs" title="Present on the system image but uninstalled for this user (e.g. via Canta/Shizuku)">uninstalled</span>}
|
||||||
|
{isInst && !isUninstalledForUser && isDisabled && <span className="badge-yellow text-xs">disabled</span>}
|
||||||
{p.deps && p.deps.length > 0 && (
|
{p.deps && p.deps.length > 0 && (
|
||||||
<span className="badge-gray text-xs" title={`Depends on: ${p.deps.join(', ')}`}>has deps</span>
|
<span className="badge-gray text-xs" title={`Depends on: ${p.deps.join(', ')}`}>has deps</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
import { useState, useCallback, useMemo } from 'react'
|
import { useState, useCallback, useMemo } from 'react'
|
||||||
import { Package, RefreshCw, Search, Download, Trash2, Power, PowerOff, X } from 'lucide-react'
|
import { Package, RefreshCw, Search, Download, Trash2, Power, PowerOff, RotateCcw, X } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
ListPackages, UninstallMultiplePackages, DisableMultiplePackages,
|
ListPackages, UninstallMultiplePackages, DisableMultiplePackages,
|
||||||
EnableMultiplePackages, PullApk, ClearData, ForceStopPackage,
|
EnableMultiplePackages, RestoreMultiplePackages, PullApk, ClearData, ForceStopPackage,
|
||||||
SelectFileForInstall, InstallPackage
|
SelectFileForInstall, InstallPackage,
|
||||||
|
UninstallPackage, DisablePackage, EnablePackage, RestorePackage
|
||||||
} from '../../lib/wails'
|
} from '../../lib/wails'
|
||||||
import { notify } from '../../lib/notify'
|
import { notify } from '../../lib/notify'
|
||||||
import { ensureDangerUnlocked } from '../../lib/applock'
|
import { ensureDangerUnlocked } from '../../lib/applock'
|
||||||
import type { PackageInfo } from '../../lib/types'
|
import type { PackageInfo } from '../../lib/types'
|
||||||
|
|
||||||
type Filter = 'all' | 'user' | 'system'
|
type Filter = 'all' | 'user' | 'system'
|
||||||
|
type StateFilter = 'all' | 'enabled' | 'disabled' | 'uninstalled'
|
||||||
|
|
||||||
export default function ViewPackages() {
|
export default function ViewPackages() {
|
||||||
const [packages, setPackages] = useState<PackageInfo[]>([])
|
const [packages, setPackages] = useState<PackageInfo[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [filter, setFilter] = useState<Filter>('user')
|
const [filter, setFilter] = useState<Filter>('user')
|
||||||
|
const [stateFilter, setStateFilter] = useState<StateFilter>('all')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||||
const [activeCtx, setActiveCtx] = useState<string | null>(null)
|
const [activeCtx, setActiveCtx] = useState<string | null>(null)
|
||||||
|
|
@ -36,8 +39,16 @@ export default function ViewPackages() {
|
||||||
const filtered = useMemo(() =>
|
const filtered = useMemo(() =>
|
||||||
packages
|
packages
|
||||||
.filter(p => p.packageName.toLowerCase().includes(search.toLowerCase()))
|
.filter(p => p.packageName.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
.filter(p => {
|
||||||
|
switch (stateFilter) {
|
||||||
|
case 'enabled': return p.isInstalled && p.isEnabled
|
||||||
|
case 'disabled': return p.isInstalled && !p.isEnabled
|
||||||
|
case 'uninstalled': return !p.isInstalled
|
||||||
|
default: return true
|
||||||
|
}
|
||||||
|
})
|
||||||
.sort((a, b) => a.packageName.localeCompare(b.packageName)),
|
.sort((a, b) => a.packageName.localeCompare(b.packageName)),
|
||||||
[packages, search]
|
[packages, search, stateFilter]
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleSelect = (pkg: string) => {
|
const toggleSelect = (pkg: string) => {
|
||||||
|
|
@ -120,6 +131,67 @@ export default function ViewPackages() {
|
||||||
setActiveCtx(null)
|
setActiveCtx(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single-package state changes, mirroring the batch toolbar actions but
|
||||||
|
// reachable per-row from the "..." menu without needing to select first.
|
||||||
|
const handleUninstall = async (pkg: string) => {
|
||||||
|
setActiveCtx(null)
|
||||||
|
if (!confirm(`Uninstall ${pkg} for the current user?`)) return
|
||||||
|
if (!(await ensureDangerUnlocked())) return
|
||||||
|
const id = notify.loading(`Uninstalling ${pkg}...`)
|
||||||
|
try {
|
||||||
|
const out = await UninstallPackage(pkg)
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.success(out)
|
||||||
|
load(filter)
|
||||||
|
} catch (e: any) {
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDisable = async (pkg: string) => {
|
||||||
|
setActiveCtx(null)
|
||||||
|
if (!(await ensureDangerUnlocked())) return
|
||||||
|
const id = notify.loading(`Disabling ${pkg}...`)
|
||||||
|
try {
|
||||||
|
const out = await DisablePackage(pkg)
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.success(out)
|
||||||
|
load(filter)
|
||||||
|
} catch (e: any) {
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEnable = async (pkg: string) => {
|
||||||
|
setActiveCtx(null)
|
||||||
|
const id = notify.loading(`Enabling ${pkg}...`)
|
||||||
|
try {
|
||||||
|
const out = await EnablePackage(pkg)
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.success(out)
|
||||||
|
load(filter)
|
||||||
|
} catch (e: any) {
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRestore = async (pkg: string) => {
|
||||||
|
setActiveCtx(null)
|
||||||
|
const id = notify.loading(`Restoring ${pkg}...`)
|
||||||
|
try {
|
||||||
|
const out = await RestorePackage(pkg)
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.success(out)
|
||||||
|
load(filter)
|
||||||
|
} catch (e: any) {
|
||||||
|
notify.dismiss(id)
|
||||||
|
notify.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
|
|
@ -154,6 +226,18 @@ export default function ViewPackages() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="input text-xs"
|
||||||
|
value={stateFilter}
|
||||||
|
onChange={e => setStateFilter(e.target.value as StateFilter)}
|
||||||
|
title="Filter by state"
|
||||||
|
>
|
||||||
|
<option value="all">All states</option>
|
||||||
|
<option value="enabled">Enabled</option>
|
||||||
|
<option value="disabled">Disabled</option>
|
||||||
|
<option value="uninstalled">Uninstalled</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<div className="w-px h-5 bg-bg-border" />
|
<div className="w-px h-5 bg-bg-border" />
|
||||||
|
|
||||||
<button onClick={handleInstall} className="btn-primary text-xs">
|
<button onClick={handleInstall} className="btn-primary text-xs">
|
||||||
|
|
@ -171,6 +255,9 @@ export default function ViewPackages() {
|
||||||
<button onClick={() => batchOp('Enabling', EnableMultiplePackages)} className="btn-ghost text-xs">
|
<button onClick={() => batchOp('Enabling', EnableMultiplePackages)} className="btn-ghost text-xs">
|
||||||
<Power size={13} /> Enable ({selected.size})
|
<Power size={13} /> Enable ({selected.size})
|
||||||
</button>
|
</button>
|
||||||
|
<button onClick={() => batchOp('Restoring', RestoreMultiplePackages)} className="btn-ghost text-xs text-accent-green">
|
||||||
|
<RotateCcw size={13} /> Restore ({selected.size})
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -219,9 +306,13 @@ export default function ViewPackages() {
|
||||||
/>
|
/>
|
||||||
<span className="mono text-text-secondary truncate">{pkg.packageName}</span>
|
<span className="mono text-text-secondary truncate">{pkg.packageName}</span>
|
||||||
<span>
|
<span>
|
||||||
<span className={pkg.isEnabled ? 'badge-green' : 'badge-red'}>
|
{!pkg.isInstalled ? (
|
||||||
{pkg.isEnabled ? 'enabled' : 'disabled'}
|
<span className="badge-gray" title="Present on the system image but uninstalled for this user (e.g. via Canta/Shizuku)">uninstalled</span>
|
||||||
</span>
|
) : (
|
||||||
|
<span className={pkg.isEnabled ? 'badge-green' : 'badge-red'}>
|
||||||
|
{pkg.isEnabled ? 'enabled' : 'disabled'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
|
|
@ -232,15 +323,42 @@ export default function ViewPackages() {
|
||||||
</button>
|
</button>
|
||||||
{activeCtx === pkg.packageName && (
|
{activeCtx === pkg.packageName && (
|
||||||
<div className="absolute right-0 top-full mt-1 bg-bg-raised border border-bg-border rounded shadow-xl z-10 min-w-[160px]">
|
<div className="absolute right-0 top-full mt-1 bg-bg-raised border border-bg-border rounded shadow-xl z-10 min-w-[160px]">
|
||||||
<button onClick={() => handlePullApk(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-text-secondary hover:text-text-primary hover:bg-bg-surface">
|
{!pkg.isInstalled && (
|
||||||
<Download size={12} /> Pull APK
|
<>
|
||||||
</button>
|
<button onClick={() => handleRestore(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-accent-green hover:bg-bg-surface">
|
||||||
<button onClick={() => handleClearData(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
<RotateCcw size={12} /> Restore
|
||||||
<X size={12} /> Clear Data
|
</button>
|
||||||
</button>
|
<button onClick={() => handleDisable(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
||||||
<button onClick={() => handleForceStop(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-danger hover:bg-bg-surface">
|
<PowerOff size={12} /> Disable
|
||||||
<PowerOff size={12} /> Force Stop
|
</button>
|
||||||
</button>
|
</>
|
||||||
|
)}
|
||||||
|
{pkg.isInstalled && (
|
||||||
|
<>
|
||||||
|
<button onClick={() => handlePullApk(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-text-secondary hover:text-text-primary hover:bg-bg-surface">
|
||||||
|
<Download size={12} /> Pull APK
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleClearData(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
||||||
|
<X size={12} /> Clear Data
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleForceStop(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-danger hover:bg-bg-surface">
|
||||||
|
<PowerOff size={12} /> Force Stop
|
||||||
|
</button>
|
||||||
|
<div className="h-px bg-bg-border my-1" />
|
||||||
|
{pkg.isEnabled ? (
|
||||||
|
<button onClick={() => handleDisable(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-warn hover:bg-bg-surface">
|
||||||
|
<PowerOff size={12} /> Disable
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => handleEnable(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-accent-green hover:bg-bg-surface">
|
||||||
|
<Power size={12} /> Enable
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button onClick={() => handleUninstall(pkg.packageName)} className="flex items-center gap-2 w-full px-3 py-2 text-xs text-danger hover:bg-bg-surface">
|
||||||
|
<Trash2 size={12} /> Uninstall
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Shield, RefreshCw, Check, AlertTriangle, Palette, Lock } from 'lucide-react'
|
import { Shield, RefreshCw, Check, AlertTriangle, Palette, Lock, ChevronDown, ChevronRight } from 'lucide-react'
|
||||||
import { GetBinaryInfo, SetAdbPath, SetFastbootPath, AppLockStatus, SetAppPassword, DisableAppLock, SetRequireForDanger } from '../../lib/wails'
|
import { GetBinaryInfo, SetAdbPath, SetFastbootPath, AppLockStatus, SetAppPassword, DisableAppLock, SetRequireForDanger, OpenURL } from '../../lib/wails'
|
||||||
import { notify } from '../../lib/notify'
|
import { notify } from '../../lib/notify'
|
||||||
import { refreshAppLockStatus } from '../../lib/applock'
|
import { refreshAppLockStatus } from '../../lib/applock'
|
||||||
import { applyTheme, getTheme, THEMES, type Theme } from '../../lib/theme'
|
import { applyTheme, getTheme, THEMES, type Theme } from '../../lib/theme'
|
||||||
import { getCustomAccent, setCustomAccent, getCustomFont, setCustomFont, FONT_OPTIONS } from '../../lib/appearance'
|
import {
|
||||||
|
getCustomAccent, setCustomAccent, getCustomTextColor, setCustomTextColor,
|
||||||
|
getCustomFont, setCustomFont, FONT_OPTIONS,
|
||||||
|
getCustomFontSize, setCustomFontSize, FONT_SIZE_MIN, FONT_SIZE_MAX, FONT_SIZE_DEFAULT
|
||||||
|
} from '../../lib/appearance'
|
||||||
import { getSidebarPosition, setSidebarPosition, SIDEBAR_POSITIONS, getSidebarLabels, setSidebarLabels, type SidebarPosition } from '../../lib/layout'
|
import { getSidebarPosition, setSidebarPosition, SIDEBAR_POSITIONS, getSidebarLabels, setSidebarLabels, type SidebarPosition } from '../../lib/layout'
|
||||||
import { getRootTools, setRootTools, getHiddenViews, setHiddenViews, TOGGLEABLE_VIEWS, getMuteNoDevice, setMuteNoDevice } from '../../lib/featureflags'
|
import { getRootTools, setRootTools, getHiddenViews, setHiddenViews, TOGGLEABLE_VIEWS, getMuteNoDevice, setMuteNoDevice } from '../../lib/featureflags'
|
||||||
import { resetDismissed } from '../../lib/dismissible'
|
import { resetDismissed } from '../../lib/dismissible'
|
||||||
|
|
@ -18,13 +22,18 @@ export default function ViewSettings() {
|
||||||
const [sidebarPos, setSidebarPos] = useState<SidebarPosition>(getSidebarPosition())
|
const [sidebarPos, setSidebarPos] = useState<SidebarPosition>(getSidebarPosition())
|
||||||
const [sidebarLabels, setSidebarLabelsState] = useState<boolean>(getSidebarLabels())
|
const [sidebarLabels, setSidebarLabelsState] = useState<boolean>(getSidebarLabels())
|
||||||
const [rootTools, setRootToolsState] = useState<boolean>(getRootTools())
|
const [rootTools, setRootToolsState] = useState<boolean>(getRootTools())
|
||||||
|
const [aboutOpen, setAboutOpen] = useState(false)
|
||||||
|
|
||||||
const [customAccent, setCustomAccentState] = useState<string>(getCustomAccent())
|
const [customAccent, setCustomAccentState] = useState<string>(getCustomAccent())
|
||||||
|
const [customTextColor, setCustomTextColorState] = useState<string>(getCustomTextColor())
|
||||||
const [customFont, setCustomFontState] = useState<string>(getCustomFont())
|
const [customFont, setCustomFontState] = useState<string>(getCustomFont())
|
||||||
|
const [customFontSize, setCustomFontSizeState] = useState<number>(getCustomFontSize())
|
||||||
|
|
||||||
const changeTheme = (t: Theme) => { setTheme(t); applyTheme(t) }
|
const changeTheme = (t: Theme) => { setTheme(t); applyTheme(t) }
|
||||||
const changeAccent = (hex: string | null) => { setCustomAccentState(hex || ''); setCustomAccent(hex) }
|
const changeAccent = (hex: string | null) => { setCustomAccentState(hex || ''); setCustomAccent(hex) }
|
||||||
|
const changeTextColor = (hex: string | null) => { setCustomTextColorState(hex || ''); setCustomTextColor(hex) }
|
||||||
const changeFont = (id: string) => { setCustomFontState(id); setCustomFont(id || null) }
|
const changeFont = (id: string) => { setCustomFontState(id); setCustomFont(id || null) }
|
||||||
|
const changeFontSize = (px: number) => { setCustomFontSizeState(px); setCustomFontSize(px) }
|
||||||
const changeSidebarPos = (p: SidebarPosition) => { setSidebarPos(p); setSidebarPosition(p) }
|
const changeSidebarPos = (p: SidebarPosition) => { setSidebarPos(p); setSidebarPosition(p) }
|
||||||
const changeSidebarLabels = (on: boolean) => { setSidebarLabelsState(on); setSidebarLabels(on) }
|
const changeSidebarLabels = (on: boolean) => { setSidebarLabelsState(on); setSidebarLabels(on) }
|
||||||
const changeRootTools = (on: boolean) => { setRootToolsState(on); setRootTools(on) }
|
const changeRootTools = (on: boolean) => { setRootToolsState(on); setRootTools(on) }
|
||||||
|
|
@ -160,8 +169,8 @@ export default function ViewSettings() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom accent colour + font — system-wide overrides on top of the theme */}
|
{/* Custom accent/text colour + font/size — system-wide overrides on top of the theme */}
|
||||||
<div className="pt-1 grid grid-cols-2 gap-4">
|
<div className="pt-1 grid grid-cols-4 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-text-muted mb-1.5">Custom accent colour (overrides the theme accent everywhere)</p>
|
<p className="text-xs text-text-muted mb-1.5">Custom accent colour (overrides the theme accent everywhere)</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -178,6 +187,22 @@ export default function ViewSettings() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-text-muted mb-1.5">Custom text colour (overrides the theme's primary text colour)</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={customTextColor || '#c6d0f5'}
|
||||||
|
onChange={e => changeTextColor(e.target.value)}
|
||||||
|
className="h-8 w-12 rounded border border-bg-border bg-bg-raised cursor-pointer p-0.5"
|
||||||
|
title="Pick a custom text colour"
|
||||||
|
/>
|
||||||
|
<span className="mono text-xs text-text-secondary">{customTextColor || 'theme default'}</span>
|
||||||
|
{customTextColor && (
|
||||||
|
<button onClick={() => changeTextColor(null)} className="btn-ghost text-xs ml-auto">Reset</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-text-muted mb-1.5">Font (applied app-wide)</p>
|
<p className="text-xs text-text-muted mb-1.5">Font (applied app-wide)</p>
|
||||||
<select
|
<select
|
||||||
|
|
@ -188,6 +213,23 @@ export default function ViewSettings() {
|
||||||
{FONT_OPTIONS.map(f => <option key={f.id} value={f.id}>{f.label}</option>)}
|
{FONT_OPTIONS.map(f => <option key={f.id} value={f.id}>{f.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-text-muted mb-1.5">Font size ({customFontSize || FONT_SIZE_DEFAULT}px)</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={FONT_SIZE_MIN}
|
||||||
|
max={FONT_SIZE_MAX}
|
||||||
|
value={customFontSize || FONT_SIZE_DEFAULT}
|
||||||
|
onChange={e => changeFontSize(Number(e.target.value))}
|
||||||
|
className="flex-1 accent-accent-green"
|
||||||
|
title="Adjust the app-wide base font size"
|
||||||
|
/>
|
||||||
|
{customFontSize > 0 && (
|
||||||
|
<button onClick={() => changeFontSize(0)} className="btn-ghost text-xs">Reset</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-text-muted pt-1">Sidebar position. Applies instantly and is remembered.</p>
|
<p className="text-xs text-text-muted pt-1">Sidebar position. Applies instantly and is remembered.</p>
|
||||||
|
|
@ -469,12 +511,26 @@ export default function ViewSettings() {
|
||||||
|
|
||||||
{/* About */}
|
{/* About */}
|
||||||
<div className="card p-4 space-y-2">
|
<div className="card p-4 space-y-2">
|
||||||
<p className="section-title">About</p>
|
<button
|
||||||
<div className="text-xs text-text-muted space-y-1">
|
onClick={() => setAboutOpen(o => !o)}
|
||||||
<p>ATK (Android Toolkit) — an all-in-one ADB GUI for Android power users and bug hunters</p>
|
className="flex items-center gap-2 w-full text-left"
|
||||||
<p>All commands use discrete argument passing — no shell string building, no injection vectors</p>
|
>
|
||||||
<p>Built with Wails v2 (Go + React) · github.com/jegly/ATK</p>
|
{aboutOpen ? <ChevronDown size={13} className="text-text-muted" /> : <ChevronRight size={13} className="text-text-muted" />}
|
||||||
</div>
|
<p className="section-title">About</p>
|
||||||
|
</button>
|
||||||
|
{aboutOpen && (
|
||||||
|
<div className="text-xs text-text-muted space-y-1 pl-5">
|
||||||
|
<p>ATK (Android Toolkit) — an all-in-one ADB GUI for Android power users</p>
|
||||||
|
<p>All commands use discrete argument passing — no shell string building, no injection vectors</p>
|
||||||
|
<p>Built with Wails v2 (Go + React) · github.com/jegly/ATK</p>
|
||||||
|
<p>
|
||||||
|
Jesse Li-Yates ·{' '}
|
||||||
|
<button onClick={() => OpenURL('https://www.jegly.xyz')} className="text-accent-green hover:underline">
|
||||||
|
www.jegly.xyz
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,33 @@
|
||||||
// removes the inline prop so the theme's own value shows through again.
|
// removes the inline prop so the theme's own value shows through again.
|
||||||
|
|
||||||
const ACCENT_KEY = 'atk-custom-accent'
|
const ACCENT_KEY = 'atk-custom-accent'
|
||||||
|
const TEXT_COLOR_KEY = 'atk-custom-text-color'
|
||||||
const FONT_KEY = 'atk-custom-font'
|
const FONT_KEY = 'atk-custom-font'
|
||||||
|
const FONT_SIZE_KEY = 'atk-custom-font-size'
|
||||||
|
|
||||||
// Built-in font choices. '' = use the theme/app default. The two @fontsource
|
// Base UI font size in px. The slider in Settings ranges over these bounds;
|
||||||
// families are bundled; the rest are system generics that always resolve.
|
// FONT_SIZE_DEFAULT matches global.css's hardcoded fallback (var(--app-font-size, 14px)).
|
||||||
|
export const FONT_SIZE_MIN = 12
|
||||||
|
export const FONT_SIZE_MAX = 18
|
||||||
|
export const FONT_SIZE_DEFAULT = 14
|
||||||
|
|
||||||
|
// Built-in font choices. '' = use the theme/app default. The @fontsource
|
||||||
|
// families and the 8 self-hosted display fonts (styles/fonts.css) are
|
||||||
|
// bundled; the "system-*" entries are generics that always resolve.
|
||||||
export const FONT_OPTIONS: { id: string; label: string; stack: string }[] = [
|
export const FONT_OPTIONS: { id: string; label: string; stack: string }[] = [
|
||||||
{ id: '', label: 'Default (IBM Plex Sans)', stack: '' },
|
{ id: '', label: 'Default (IBM Plex Sans)', stack: '' },
|
||||||
{ id: 'jetbrains', label: 'JetBrains Mono', stack: "'JetBrains Mono', monospace" },
|
{ id: 'jetbrains', label: 'JetBrains Mono', stack: "'JetBrains Mono', monospace" },
|
||||||
{ id: 'system-sans', label: 'System Sans', stack: 'system-ui, sans-serif' },
|
{ id: 'system-sans', label: 'System Sans', stack: 'system-ui, sans-serif' },
|
||||||
{ id: 'system-serif', label: 'System Serif', stack: 'Georgia, \'Times New Roman\', serif' },
|
{ id: 'system-serif', label: 'System Serif', stack: 'Georgia, \'Times New Roman\', serif' },
|
||||||
{ id: 'system-mono', label: 'System Monospace', stack: 'ui-monospace, \'Cascadia Code\', \'Courier New\', monospace' },
|
{ id: 'system-mono', label: 'System Monospace', stack: 'ui-monospace, \'Cascadia Code\', \'Courier New\', monospace' },
|
||||||
|
{ id: 'dotgothic16', label: 'DotGothic16', stack: "'DotGothic16', sans-serif" },
|
||||||
|
{ id: 'geist-pixel', label: 'Geist Pixel', stack: "'Geist Pixel', monospace" },
|
||||||
|
{ id: 'gugi', label: 'Gugi', stack: "'Gugi', sans-serif" },
|
||||||
|
{ id: 'orbitron', label: 'Orbitron', stack: "'Orbitron', sans-serif" },
|
||||||
|
{ id: 'playfair-display', label: 'Playfair Display', stack: "'Playfair Display', serif" },
|
||||||
|
{ id: 'press-start-2p', label: 'Press Start 2P', stack: "'Press Start 2P', monospace" },
|
||||||
|
{ id: 'space-grotesk', label: 'Space Grotesk', stack: "'Space Grotesk', sans-serif" },
|
||||||
|
{ id: 'vt323', label: 'VT323', stack: "'VT323', monospace" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function hexToChannels(hex: string): string | null {
|
export function hexToChannels(hex: string): string | null {
|
||||||
|
|
@ -32,20 +49,39 @@ function darkenChannels(ch: string, f: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomAccent(): string { return localStorage.getItem(ACCENT_KEY) || '' }
|
export function getCustomAccent(): string { return localStorage.getItem(ACCENT_KEY) || '' }
|
||||||
|
export function getCustomTextColor(): string { return localStorage.getItem(TEXT_COLOR_KEY) || '' }
|
||||||
export function getCustomFont(): string { return localStorage.getItem(FONT_KEY) || '' }
|
export function getCustomFont(): string { return localStorage.getItem(FONT_KEY) || '' }
|
||||||
|
|
||||||
|
// Returns 0 when unset (theme default applies via the CSS fallback).
|
||||||
|
export function getCustomFontSize(): number {
|
||||||
|
const v = parseInt(localStorage.getItem(FONT_SIZE_KEY) || '', 10)
|
||||||
|
return Number.isFinite(v) && v >= FONT_SIZE_MIN && v <= FONT_SIZE_MAX ? v : 0
|
||||||
|
}
|
||||||
|
|
||||||
export function setCustomAccent(hex: string | null): void {
|
export function setCustomAccent(hex: string | null): void {
|
||||||
if (hex && hexToChannels(hex)) localStorage.setItem(ACCENT_KEY, hex)
|
if (hex && hexToChannels(hex)) localStorage.setItem(ACCENT_KEY, hex)
|
||||||
else localStorage.removeItem(ACCENT_KEY)
|
else localStorage.removeItem(ACCENT_KEY)
|
||||||
applyAppearance()
|
applyAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCustomTextColor(hex: string | null): void {
|
||||||
|
if (hex && hexToChannels(hex)) localStorage.setItem(TEXT_COLOR_KEY, hex)
|
||||||
|
else localStorage.removeItem(TEXT_COLOR_KEY)
|
||||||
|
applyAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
export function setCustomFont(id: string | null): void {
|
export function setCustomFont(id: string | null): void {
|
||||||
if (id) localStorage.setItem(FONT_KEY, id)
|
if (id) localStorage.setItem(FONT_KEY, id)
|
||||||
else localStorage.removeItem(FONT_KEY)
|
else localStorage.removeItem(FONT_KEY)
|
||||||
applyAppearance()
|
applyAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCustomFontSize(px: number | null): void {
|
||||||
|
if (px && px >= FONT_SIZE_MIN && px <= FONT_SIZE_MAX) localStorage.setItem(FONT_SIZE_KEY, String(px))
|
||||||
|
else localStorage.removeItem(FONT_SIZE_KEY)
|
||||||
|
applyAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
// applyAppearance (re)applies the stored overrides. Call on boot and after a change.
|
// applyAppearance (re)applies the stored overrides. Call on boot and after a change.
|
||||||
export function applyAppearance(): void {
|
export function applyAppearance(): void {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
|
|
@ -60,8 +96,20 @@ export function applyAppearance(): void {
|
||||||
root.style.removeProperty('--accent-dim')
|
root.style.removeProperty('--accent-dim')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textColor = getCustomTextColor()
|
||||||
|
const tch = textColor ? hexToChannels(textColor) : null
|
||||||
|
if (tch) {
|
||||||
|
root.style.setProperty('--text-primary', tch)
|
||||||
|
} else {
|
||||||
|
root.style.removeProperty('--text-primary')
|
||||||
|
}
|
||||||
|
|
||||||
const fontId = getCustomFont()
|
const fontId = getCustomFont()
|
||||||
const stack = FONT_OPTIONS.find(f => f.id === fontId)?.stack
|
const stack = FONT_OPTIONS.find(f => f.id === fontId)?.stack
|
||||||
if (stack) root.style.setProperty('--app-font', stack)
|
if (stack) root.style.setProperty('--app-font', stack)
|
||||||
else root.style.removeProperty('--app-font')
|
else root.style.removeProperty('--app-font')
|
||||||
|
|
||||||
|
const fontSize = getCustomFontSize()
|
||||||
|
if (fontSize) root.style.setProperty('--app-font-size', `${fontSize}px`)
|
||||||
|
else root.style.removeProperty('--app-font-size')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,19 @@ export const TOGGLEABLE_VIEWS: { view: string; label: string }[] = [
|
||||||
{ view: 'files', label: 'Files' },
|
{ view: 'files', label: 'Files' },
|
||||||
{ view: 'mirror', label: 'Screen Mirror' },
|
{ view: 'mirror', label: 'Screen Mirror' },
|
||||||
{ view: 'packages', label: 'Packages' },
|
{ view: 'packages', label: 'Packages' },
|
||||||
|
{ view: 'apkinstaller', label: 'APK Installer' },
|
||||||
{ view: 'debloater', label: 'Debloater' },
|
{ view: 'debloater', label: 'Debloater' },
|
||||||
{ view: 'shell', label: 'Shell' },
|
{ view: 'shell', label: 'Shell' },
|
||||||
{ view: 'logcat', label: 'Logcat' },
|
{ view: 'logcat', label: 'Logcat' },
|
||||||
{ view: 'appinspect', label: 'App Inspector' },
|
{ view: 'appinspect', label: 'App Inspector' },
|
||||||
|
{ view: 'intentlab', label: 'Intent Lab' },
|
||||||
{ view: 'apkaudit', label: 'APK Audit' },
|
{ view: 'apkaudit', label: 'APK Audit' },
|
||||||
{ view: 'certs', label: 'Certificates' },
|
{ view: 'certs', label: 'Certificates' },
|
||||||
{ view: 'backup', label: 'Backup' },
|
{ view: 'backup', label: 'Backup' },
|
||||||
{ view: 'props', label: 'Prop Editor' },
|
{ view: 'props', label: 'Prop Editor' },
|
||||||
{ view: 'utilities', label: 'Utilities' },
|
{ view: 'utilities', label: 'Utilities' },
|
||||||
{ view: 'flasher', label: 'Flasher' },
|
{ view: 'flasher', label: 'Flasher' },
|
||||||
|
{ view: 'gsiloader', label: 'GSI Loader' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function getHiddenViews(): string[] {
|
export function getHiddenViews(): string[] {
|
||||||
|
|
|
||||||
13
frontend/src/lib/quitgate.ts
Normal file
13
frontend/src/lib/quitgate.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Trigger for the close-confirmation dialog (QuitGate.tsx). The title bar's
|
||||||
|
// close button calls requestQuitConfirm() instead of quitting directly, so
|
||||||
|
// the user picks Minimize-to-tray vs Quit rather than the window just vanishing.
|
||||||
|
let host: (() => void) | null = null
|
||||||
|
|
||||||
|
export function _registerQuitHost(fn: () => void): () => void {
|
||||||
|
host = fn
|
||||||
|
return () => { host = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestQuitConfirm() {
|
||||||
|
host?.()
|
||||||
|
}
|
||||||
36
frontend/src/lib/scrollfix.ts
Normal file
36
frontend/src/lib/scrollfix.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Works around a known Chromium/WebView2 bug where a native scrollbar-thumb
|
||||||
|
// drag gets stuck "captured" after a click - typically triggered by precision
|
||||||
|
// touchpad drivers that emit mousemove without a matching mouseup right after
|
||||||
|
// the click. Symptom: click a scrollbar once, then any further pointer motion
|
||||||
|
// keeps dragging it as if the button were still held down. Long lists (the
|
||||||
|
// Packages / Debloater views) are where this is actually noticed.
|
||||||
|
//
|
||||||
|
// Only acts if we saw a mousedown with no matching mouseup yet - `e.buttons
|
||||||
|
// === 0` on a subsequent mousemove then means the OS-reported button state
|
||||||
|
// disagrees with what we're tracking, i.e. the real mouseup happened but
|
||||||
|
// wasn't delivered as an event. That's the stuck-drag signature; a synthetic
|
||||||
|
// mouseup nudges the browser's internal scrollbar-drag state to release.
|
||||||
|
export function installScrollbarDragFix() {
|
||||||
|
let mouseIsDown = false
|
||||||
|
|
||||||
|
window.addEventListener('mousedown', () => { mouseIsDown = true }, { capture: true })
|
||||||
|
window.addEventListener('mouseup', () => { mouseIsDown = false }, { capture: true })
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
'mousemove',
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (mouseIsDown && e.buttons === 0) {
|
||||||
|
mouseIsDown = false
|
||||||
|
window.dispatchEvent(
|
||||||
|
new MouseEvent('mouseup', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
clientX: e.clientX,
|
||||||
|
clientY: e.clientY,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ capture: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,13 @@ export const THEMES: ThemeDef[] = [
|
||||||
{ id: 'ocean', label: 'Ocean', hint: 'Muted blue-grey', swatch: ['#2b303b', '#343d46', '#8fa1b3', '#c0c5ce'] },
|
{ id: 'ocean', label: 'Ocean', hint: 'Muted blue-grey', swatch: ['#2b303b', '#343d46', '#8fa1b3', '#c0c5ce'] },
|
||||||
{ id: 'kokuban', label: 'Kokuban', hint: 'Chalkboard green', swatch: ['#1f3526', '#274030', '#f2e9c8', '#f0f0e8'] },
|
{ id: 'kokuban', label: 'Kokuban', hint: 'Chalkboard green', swatch: ['#1f3526', '#274030', '#f2e9c8', '#f0f0e8'] },
|
||||||
{ id: 'mono-cyan', label: 'Mono Cyan', hint: 'Monochrome cyan glow', swatch: ['#081414', '#0e1f1f', '#00d0d0', '#c8f0f0'] },
|
{ id: 'mono-cyan', label: 'Mono Cyan', hint: 'Monochrome cyan glow', swatch: ['#081414', '#0e1f1f', '#00d0d0', '#c8f0f0'] },
|
||||||
|
// More additions
|
||||||
|
{ id: 'nord', label: 'Nord', hint: 'Arctic blue-grey with frost green', swatch: ['#2e3440', '#3b4252', '#a3be8c', '#eceff4'] },
|
||||||
|
{ id: 'tokyo-night', label: 'Tokyo Night', hint: 'Neon purple & blue on midnight', swatch: ['#1a1b26', '#16161e', '#9ece6a', '#c0caf5'] },
|
||||||
|
{ id: 'solarized-dark', label: 'Solarized Dark', hint: 'Low-contrast teal & olive', swatch: ['#002b36', '#073642', '#859900', '#93a1a1'] },
|
||||||
|
{ id: 'solarized-light', label: 'Solarized Light', hint: 'Low-contrast teal & olive, light', swatch: ['#fdf6e3', '#eee8d5', '#859900', '#586e75'] },
|
||||||
|
{ id: 'rose-pine', label: 'Rosé Pine', hint: 'Elegant muted purple & rose', swatch: ['#191724', '#1f1d2e', '#9ccfd8', '#e0def4'] },
|
||||||
|
{ id: 'everforest', label: 'Everforest', hint: 'Warm forest green, easy on the eyes', swatch: ['#2d353b', '#343f44', '#a7c080', '#d3c6aa'] },
|
||||||
]
|
]
|
||||||
|
|
||||||
export type Theme = string
|
export type Theme = string
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,15 @@ export interface FileEntry {
|
||||||
export interface PackageInfo {
|
export interface PackageInfo {
|
||||||
packageName: string
|
packageName: string
|
||||||
isEnabled: boolean
|
isEnabled: boolean
|
||||||
|
// False when the package is present on the system image but uninstalled for
|
||||||
|
// user 0 (e.g. via Canta/Shizuku, which runs `pm uninstall --user 0`).
|
||||||
|
isInstalled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApkFileInfo {
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relationship kinds mined (in Go) from a log line for the visual map.
|
// Relationship kinds mined (in Go) from a log line for the visual map.
|
||||||
|
|
@ -251,6 +260,7 @@ export type View =
|
||||||
| 'files'
|
| 'files'
|
||||||
| 'mirror'
|
| 'mirror'
|
||||||
| 'packages'
|
| 'packages'
|
||||||
|
| 'apkinstaller'
|
||||||
| 'debloater'
|
| 'debloater'
|
||||||
| 'shell'
|
| 'shell'
|
||||||
| 'logcat'
|
| 'logcat'
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@ export const GetBinaryInfo = () => window['go']['main']['App']['GetBinaryInfo'](
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const CheckSystemRequirements = () => window['go']['main']['App']['CheckSystemRequirements']()
|
export const CheckSystemRequirements = () => window['go']['main']['App']['CheckSystemRequirements']()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
export const ShowWindow = () => window['go']['main']['App']['ShowWindow']()
|
||||||
|
// @ts-ignore
|
||||||
|
export const HideWindow = () => window['go']['main']['App']['HideWindow']()
|
||||||
|
// @ts-ignore
|
||||||
|
export const QuitApp = () => window['go']['main']['App']['QuitApp']()
|
||||||
|
// @ts-ignore
|
||||||
|
export const TrayAvailable = () => window['go']['main']['App']['TrayAvailable']()
|
||||||
|
// @ts-ignore
|
||||||
|
export const OpenURL = (url: string) => window['go']['main']['App']['OpenURL'](url)
|
||||||
|
// @ts-ignore
|
||||||
export const SetAdbPath = (path: string) => window['go']['main']['App']['SetAdbPath'](path)
|
export const SetAdbPath = (path: string) => window['go']['main']['App']['SetAdbPath'](path)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const SetFastbootPath = (path: string) => window['go']['main']['App']['SetFastbootPath'](path)
|
export const SetFastbootPath = (path: string) => window['go']['main']['App']['SetFastbootPath'](path)
|
||||||
|
|
@ -99,9 +109,21 @@ export const EnableMultiplePackages = (pkgs: string[]) => window['go']['main']['
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const RestoreMultiplePackages = (pkgs: string[]) => window['go']['main']['App']['RestoreMultiplePackages'](pkgs)
|
export const RestoreMultiplePackages = (pkgs: string[]) => window['go']['main']['App']['RestoreMultiplePackages'](pkgs)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
export const RestorePackage = (pkg: string) => window['go']['main']['App']['RestorePackage'](pkg)
|
||||||
|
// @ts-ignore
|
||||||
export const SelectFileForInstall = () => window['go']['main']['App']['SelectFileForInstall']()
|
export const SelectFileForInstall = () => window['go']['main']['App']['SelectFileForInstall']()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const SideloadPackage = (path: string) => window['go']['main']['App']['SideloadPackage'](path)
|
export const SideloadPackage = (path: string) => window['go']['main']['App']['SideloadPackage'](path)
|
||||||
|
// @ts-ignore
|
||||||
|
export const SelectApkFolder = () => window['go']['main']['App']['SelectApkFolder']()
|
||||||
|
// @ts-ignore
|
||||||
|
export const SelectMultipleApkFiles = () => window['go']['main']['App']['SelectMultipleApkFiles']()
|
||||||
|
// @ts-ignore
|
||||||
|
export const ListApksInFolder = (folder: string) => window['go']['main']['App']['ListApksInFolder'](folder)
|
||||||
|
// @ts-ignore
|
||||||
|
export const StatApkFiles = (paths: string[]) => window['go']['main']['App']['StatApkFiles'](paths)
|
||||||
|
// @ts-ignore
|
||||||
|
export const InstallApksWithProgress = (paths: string[]) => window['go']['main']['App']['InstallApksWithProgress'](paths)
|
||||||
|
|
||||||
// Shell
|
// Shell
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,16 @@ import '@fontsource/ibm-plex-sans/500.css'
|
||||||
import '@fontsource/ibm-plex-sans/600.css'
|
import '@fontsource/ibm-plex-sans/600.css'
|
||||||
import '@fontsource/jetbrains-mono/400.css'
|
import '@fontsource/jetbrains-mono/400.css'
|
||||||
import '@fontsource/jetbrains-mono/500.css'
|
import '@fontsource/jetbrains-mono/500.css'
|
||||||
|
import './styles/fonts.css'
|
||||||
import './styles/global.css'
|
import './styles/global.css'
|
||||||
import { applyTheme, getTheme } from './lib/theme'
|
import { applyTheme, getTheme } from './lib/theme'
|
||||||
import { applyAppearance } from './lib/appearance'
|
import { applyAppearance } from './lib/appearance'
|
||||||
|
import { installScrollbarDragFix } from './lib/scrollfix'
|
||||||
|
|
||||||
// Apply the saved theme + custom accent/font before first paint to avoid a flash.
|
// Apply the saved theme + custom accent/font before first paint to avoid a flash.
|
||||||
applyTheme(getTheme())
|
applyTheme(getTheme())
|
||||||
applyAppearance()
|
applyAppearance()
|
||||||
|
installScrollbarDragFix()
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
|
||||||
60
frontend/src/styles/fonts.css
Normal file
60
frontend/src/styles/fonts.css
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* Self-hosted display/decorative fonts (bundled, OFL-licensed - see
|
||||||
|
src/assets/fonts/licenses/). Selectable app-wide from Settings via
|
||||||
|
lib/appearance.ts FONT_OPTIONS. Variable-font files cover their whole
|
||||||
|
weight range through a single @font-face with a weight range. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DotGothic16';
|
||||||
|
src: url('../assets/fonts/DotGothic16-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Geist Pixel';
|
||||||
|
src: url('../assets/fonts/GeistPixel-Regular-VariableFont_ELSH.ttf') format('truetype');
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Gugi';
|
||||||
|
src: url('../assets/fonts/Gugi-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Orbitron';
|
||||||
|
src: url('../assets/fonts/Orbitron-VariableFont_wght.ttf') format('truetype');
|
||||||
|
font-weight: 400 900;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Playfair Display';
|
||||||
|
src: url('../assets/fonts/PlayfairDisplay-VariableFont_wght.ttf') format('truetype');
|
||||||
|
font-weight: 400 900;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Press Start 2P';
|
||||||
|
src: url('../assets/fonts/PressStart2P-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Space Grotesk';
|
||||||
|
src: url('../assets/fonts/SpaceGrotesk-VariableFont_wght.ttf') format('truetype');
|
||||||
|
font-weight: 300 700;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'VT323';
|
||||||
|
src: url('../assets/fonts/VT323-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
@ -357,6 +357,109 @@
|
||||||
--scrollbar-hover: 79 136 136;
|
--scrollbar-hover: 79 136 136;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nord — arctic blue-grey with frost green */
|
||||||
|
:root[data-theme="nord"] {
|
||||||
|
--bg-base: 46 52 64;
|
||||||
|
--bg-surface: 59 66 82;
|
||||||
|
--bg-raised: 67 76 94;
|
||||||
|
--bg-border: 76 86 106;
|
||||||
|
--accent-green: 163 190 140;
|
||||||
|
--accent-dim: 130 152 112;
|
||||||
|
--accent-muted: 33 38 28;
|
||||||
|
--text-primary: 236 239 244;
|
||||||
|
--text-secondary: 216 222 233;
|
||||||
|
--text-muted: 76 86 106;
|
||||||
|
--danger: 191 97 106;
|
||||||
|
--warn: 235 203 139;
|
||||||
|
--scrollbar-hover: 126 133 148;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tokyo Night — neon purple & blue on midnight */
|
||||||
|
:root[data-theme="tokyo-night"] {
|
||||||
|
--bg-base: 26 27 38;
|
||||||
|
--bg-surface: 22 22 30;
|
||||||
|
--bg-raised: 36 40 59;
|
||||||
|
--bg-border: 41 46 66;
|
||||||
|
--accent-green: 158 206 106;
|
||||||
|
--accent-dim: 126 165 85;
|
||||||
|
--accent-muted: 32 41 21;
|
||||||
|
--text-primary: 192 202 245;
|
||||||
|
--text-secondary: 169 177 214;
|
||||||
|
--text-muted: 86 95 137;
|
||||||
|
--danger: 247 118 142;
|
||||||
|
--warn: 224 175 104;
|
||||||
|
--scrollbar-hover: 101 105 119;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Solarized Dark — low-contrast teal & olive */
|
||||||
|
:root[data-theme="solarized-dark"] {
|
||||||
|
--bg-base: 0 43 54;
|
||||||
|
--bg-surface: 7 54 66;
|
||||||
|
--bg-raised: 10 61 74;
|
||||||
|
--bg-border: 88 110 117;
|
||||||
|
--accent-green: 133 153 0;
|
||||||
|
--accent-dim: 106 122 0;
|
||||||
|
--accent-muted: 27 31 0;
|
||||||
|
--text-primary: 147 161 161;
|
||||||
|
--text-secondary: 131 148 150;
|
||||||
|
--text-muted: 88 110 117;
|
||||||
|
--danger: 220 50 47;
|
||||||
|
--warn: 181 137 0;
|
||||||
|
--scrollbar-hover: 135 151 156;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Solarized Light — low-contrast teal & olive, light */
|
||||||
|
:root[data-theme="solarized-light"] {
|
||||||
|
--bg-base: 253 246 227;
|
||||||
|
--bg-surface: 238 232 213;
|
||||||
|
--bg-raised: 229 221 195;
|
||||||
|
--bg-border: 201 189 151;
|
||||||
|
--accent-green: 133 153 0;
|
||||||
|
--accent-dim: 106 122 0;
|
||||||
|
--accent-muted: 233 237 209;
|
||||||
|
--text-primary: 88 110 117;
|
||||||
|
--text-secondary: 101 123 131;
|
||||||
|
--text-muted: 147 161 161;
|
||||||
|
--danger: 220 50 47;
|
||||||
|
--warn: 181 137 0;
|
||||||
|
--scrollbar-hover: 185 174 139;
|
||||||
|
}
|
||||||
|
:root[data-theme="solarized-light"] body { color-scheme: light; }
|
||||||
|
|
||||||
|
/* Rosé Pine — elegant muted purple & rose */
|
||||||
|
:root[data-theme="rose-pine"] {
|
||||||
|
--bg-base: 25 23 36;
|
||||||
|
--bg-surface: 31 29 46;
|
||||||
|
--bg-raised: 38 35 58;
|
||||||
|
--bg-border: 64 61 82;
|
||||||
|
--accent-green: 156 207 216;
|
||||||
|
--accent-dim: 125 166 173;
|
||||||
|
--accent-muted: 31 41 43;
|
||||||
|
--text-primary: 224 222 244;
|
||||||
|
--text-secondary: 144 140 170;
|
||||||
|
--text-muted: 110 106 134;
|
||||||
|
--danger: 235 111 146;
|
||||||
|
--warn: 246 193 119;
|
||||||
|
--scrollbar-hover: 117 115 130;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Everforest — warm forest green, easy on the eyes */
|
||||||
|
:root[data-theme="everforest"] {
|
||||||
|
--bg-base: 45 53 59;
|
||||||
|
--bg-surface: 52 63 68;
|
||||||
|
--bg-raised: 61 72 77;
|
||||||
|
--bg-border: 71 82 88;
|
||||||
|
--accent-green: 167 192 128;
|
||||||
|
--accent-dim: 134 154 102;
|
||||||
|
--accent-muted: 33 38 26;
|
||||||
|
--text-primary: 211 198 170;
|
||||||
|
--text-secondary: 157 169 160;
|
||||||
|
--text-muted: 122 132 120;
|
||||||
|
--danger: 230 126 128;
|
||||||
|
--warn: 219 188 127;
|
||||||
|
--scrollbar-hover: 123 130 135;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -397,10 +500,10 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
color: rgb(var(--text-primary));
|
color: rgb(var(--text-primary));
|
||||||
/* --app-font is an optional system-wide override set from Settings (see
|
/* --app-font/--app-font-size are optional system-wide overrides set from
|
||||||
lib/appearance.ts); falls back to the bundled UI font. */
|
Settings (see lib/appearance.ts); fall back to the bundled UI font/14px. */
|
||||||
font-family: var(--app-font, 'IBM Plex Sans', sans-serif);
|
font-family: var(--app-font, 'IBM Plex Sans', sans-serif);
|
||||||
font-size: 14px;
|
font-size: var(--app-font-size, 14px);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
/* Content is selectable/copyable; interactive chrome opts out below. */
|
/* Content is selectable/copyable; interactive chrome opts out below. */
|
||||||
|
|
@ -453,17 +556,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 9px;
|
||||||
height: 6px;
|
height: 9px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgb(var(--bg-border));
|
background: rgb(var(--bg-border));
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover,
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
background: rgb(var(--scrollbar-hover));
|
background: rgb(var(--scrollbar-hover));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -5,6 +5,7 @@ go 1.25.0
|
||||||
require (
|
require (
|
||||||
github.com/avast/apkparser v0.0.0-20260423123151-7fcaee440f68
|
github.com/avast/apkparser v0.0.0-20260423123151-7fcaee440f68
|
||||||
github.com/avast/apkverifier v0.0.0-20260410045523-e2781ccbddc8
|
github.com/avast/apkverifier v0.0.0-20260410045523-e2781ccbddc8
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/ncruces/zenity v0.10.14
|
github.com/ncruces/zenity v0.10.14
|
||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/wailsapp/wails/v2 v2.12.0
|
github.com/wailsapp/wails/v2 v2.12.0
|
||||||
|
|
@ -17,7 +18,6 @@ require (
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
|
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: "atk"
|
name: "atk"
|
||||||
arch: "amd64"
|
arch: "amd64"
|
||||||
platform: "linux"
|
platform: "linux"
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
section: "utils"
|
section: "utils"
|
||||||
priority: "optional"
|
priority: "optional"
|
||||||
maintainer: "jegly <https://github.com/jegly>"
|
maintainer: "jegly <https://github.com/jegly>"
|
||||||
|
|
|
||||||
|
|
@ -35,20 +35,22 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var allPkgs, disabledPkgs []string
|
var allPkgs, installedPkgs, disabledPkgs []string
|
||||||
var errAll error
|
var errAll error
|
||||||
|
|
||||||
wg.Add(2)
|
wg.Add(3)
|
||||||
|
|
||||||
// Membership = the UNFILTERED list for this category. Every installed package
|
// Membership = the UNFILTERED superset for this category, via `-u`. Every
|
||||||
// shows up here regardless of state. The old approach unioned `-e` (enabled)
|
// package that's ever been on the system image shows up here, INCLUDING
|
||||||
// and `-d` (disabled), but a ROM parks some apps in states (e.g.
|
// ones uninstalled for user 0 (Canta/Shizuku's "uninstall" of a system app
|
||||||
// DISABLED_UNTIL_USED) that neither filter reports, so system apps silently
|
// is really `pm uninstall --user 0`, which leaves the APK on disk but drops
|
||||||
// went missing from App Inspector / APK Audit. Plain `pm list packages` is a
|
// it from a plain `pm list packages`). The old approach queried without
|
||||||
// strict superset, so nothing is dropped.
|
// `-u`, which is why per-user-uninstalled system apps were invisible in
|
||||||
|
// Packages / Debloater / the App Inspector picker even though they were
|
||||||
|
// still physically present on the device.
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
output, err := a.runAdbShell(buildArgs()...)
|
output, err := a.runAdbShell(buildArgs("-u")...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errAll = err
|
errAll = err
|
||||||
return
|
return
|
||||||
|
|
@ -56,6 +58,19 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
||||||
allPkgs = parse(output)
|
allPkgs = parse(output)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Without `-u`: only packages actually installed for user 0 right now. Used
|
||||||
|
// to tell "uninstalled for user" apart from enabled/disabled — `-d`/`-e`
|
||||||
|
// alone can't do that, since a per-user-uninstalled package appears in
|
||||||
|
// neither.
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
output, err := a.runAdbShell(buildArgs()...)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
installedPkgs = parse(output)
|
||||||
|
}()
|
||||||
|
|
||||||
// `-d` is used only to flag the disabled badge. Some ROMs restrict it; that's
|
// `-d` is used only to flag the disabled badge. Some ROMs restrict it; that's
|
||||||
// non-fatal — it just means nothing gets marked disabled.
|
// non-fatal — it just means nothing gets marked disabled.
|
||||||
go func() {
|
go func() {
|
||||||
|
|
@ -73,6 +88,10 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
||||||
return nil, fmt.Errorf("failed to list packages: %w", errAll)
|
return nil, fmt.Errorf("failed to list packages: %w", errAll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
installed := make(map[string]bool, len(installedPkgs))
|
||||||
|
for _, p := range installedPkgs {
|
||||||
|
installed[p] = true
|
||||||
|
}
|
||||||
disabled := make(map[string]bool, len(disabledPkgs))
|
disabled := make(map[string]bool, len(disabledPkgs))
|
||||||
for _, p := range disabledPkgs {
|
for _, p := range disabledPkgs {
|
||||||
disabled[p] = true
|
disabled[p] = true
|
||||||
|
|
@ -85,7 +104,14 @@ func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[p] = true
|
seen[p] = true
|
||||||
packages = append(packages, PackageInfo{PackageName: p, IsEnabled: !disabled[p]})
|
isInstalled := installed[p]
|
||||||
|
// A package not installed for the user can't run either way, so it's
|
||||||
|
// never reported as enabled regardless of its internal enabled flag.
|
||||||
|
packages = append(packages, PackageInfo{
|
||||||
|
PackageName: p,
|
||||||
|
IsInstalled: isInstalled,
|
||||||
|
IsEnabled: isInstalled && !disabled[p],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return packages, nil
|
return packages, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
298
scripts/gen_tray_icon.py
Normal file
298
scripts/gen_tray_icon.py
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generates the ATK tray icon assets from a hand-specified vector path (no
|
||||||
|
image libraries available offline - this rasterizes and encodes PNG/ICO from
|
||||||
|
scratch using only the stdlib). Re-run after editing PATH_D or COLORS.
|
||||||
|
|
||||||
|
Outputs into assets/:
|
||||||
|
tray_icon.png 32x32 RGBA, ATK green - Windows/Linux tray icon
|
||||||
|
tray_icon_template.png 32x32 RGBA, black silhouette - macOS template icon
|
||||||
|
tray_icon.ico 16 + 32px, ATK green - Windows .ico container
|
||||||
|
tray_symbolic.svg vector, single-fill - Linux icon-theme install
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Material Design "alternate_email" (@) glyph, 24x24 viewBox. Single source of
|
||||||
|
# truth for every rasterized/vector output below.
|
||||||
|
PATH_D = (
|
||||||
|
"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c1.28,0 2.5,-0.2 3.68,-0.68l-0.36,-1.9"
|
||||||
|
"C14.34,19.79 13.19,20 12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8v0.9"
|
||||||
|
"c0,0.61 -0.6,1.32 -1.5,1.32s-1.5,-0.71 -1.5,-1.32V8h-2v0.68"
|
||||||
|
"C14.42,8.24 13.51,8 12.5,8C10.02,8 8,10.02 8,12.5s2.02,4.5 4.5,4.5"
|
||||||
|
"c1.14,0 2.17,-0.42 2.96,-1.11c0.55,0.66 1.42,1.11 2.29,1.11"
|
||||||
|
"c1.66,0 3.25,-1.34 3.25,-3.32V12C21,6.48 16.52,2 12,2z"
|
||||||
|
"M12.5,15c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5"
|
||||||
|
"S13.88,15 12.5,15z"
|
||||||
|
)
|
||||||
|
VIEWBOX = 24
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
ASSETS = os.path.join(ROOT, "assets")
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize(d):
|
||||||
|
return re.findall(r"[MmLlHhVvCcSsZz]|-?\d*\.?\d+(?:[eE]-?\d+)?", d)
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_cubic(p0, p1, p2, p3, segs=20):
|
||||||
|
pts = []
|
||||||
|
for i in range(1, segs + 1):
|
||||||
|
t = i / segs
|
||||||
|
mt = 1 - t
|
||||||
|
x = mt**3 * p0[0] + 3 * mt**2 * t * p1[0] + 3 * mt * t**2 * p2[0] + t**3 * p3[0]
|
||||||
|
y = mt**3 * p0[1] + 3 * mt**2 * t * p1[1] + 3 * mt * t**2 * p2[1] + t**3 * p3[1]
|
||||||
|
pts.append((x, y))
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def parse_path(d):
|
||||||
|
"""Returns a flat list of (x1,y1,x2,y2) edges (line segments), flattening
|
||||||
|
beziers, across every subpath - enough for a nonzero-winding scanline fill."""
|
||||||
|
tokens = tokenize(d)
|
||||||
|
i = 0
|
||||||
|
cmd = None
|
||||||
|
cx = cy = 0.0
|
||||||
|
start_x = start_y = 0.0
|
||||||
|
last_ctrl = None
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
def line_to(nx, ny):
|
||||||
|
edges.append((cx, cy, nx, ny))
|
||||||
|
|
||||||
|
while i < len(tokens):
|
||||||
|
tok = tokens[i]
|
||||||
|
if tok in "MmLlHhVvCcSsZz":
|
||||||
|
cmd = tok
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if cmd in ("M", "m"):
|
||||||
|
x, y = float(tokens[i]), float(tokens[i + 1])
|
||||||
|
i += 2
|
||||||
|
if cmd == "m":
|
||||||
|
x += cx
|
||||||
|
y += cy
|
||||||
|
cx, cy = x, y
|
||||||
|
start_x, start_y = x, y
|
||||||
|
cmd = "L" if cmd == "M" else "l" # subsequent pairs are implicit lineto
|
||||||
|
elif cmd in ("L", "l"):
|
||||||
|
x, y = float(tokens[i]), float(tokens[i + 1])
|
||||||
|
i += 2
|
||||||
|
if cmd == "l":
|
||||||
|
x += cx
|
||||||
|
y += cy
|
||||||
|
line_to(x, y)
|
||||||
|
cx, cy = x, y
|
||||||
|
elif cmd in ("H", "h"):
|
||||||
|
x = float(tokens[i])
|
||||||
|
i += 1
|
||||||
|
if cmd == "h":
|
||||||
|
x += cx
|
||||||
|
line_to(x, cy)
|
||||||
|
cx = x
|
||||||
|
elif cmd in ("V", "v"):
|
||||||
|
y = float(tokens[i])
|
||||||
|
i += 1
|
||||||
|
if cmd == "v":
|
||||||
|
y += cy
|
||||||
|
line_to(cx, y)
|
||||||
|
cy = y
|
||||||
|
elif cmd in ("C", "c"):
|
||||||
|
x1, y1, x2, y2, x, y = (float(tokens[i + k]) for k in range(6))
|
||||||
|
i += 6
|
||||||
|
if cmd == "c":
|
||||||
|
x1 += cx; y1 += cy; x2 += cx; y2 += cy; x += cx; y += cy
|
||||||
|
for px, py in flatten_cubic((cx, cy), (x1, y1), (x2, y2), (x, y)):
|
||||||
|
edges.append((cx, cy, px, py))
|
||||||
|
cx, cy = px, py
|
||||||
|
last_ctrl = (x2, y2)
|
||||||
|
elif cmd in ("S", "s"):
|
||||||
|
x2, y2, x, y = (float(tokens[i + k]) for k in range(4))
|
||||||
|
i += 4
|
||||||
|
if cmd == "s":
|
||||||
|
x2 += cx; y2 += cy; x += cx; y += cy
|
||||||
|
if last_ctrl:
|
||||||
|
x1, y1 = 2 * cx - last_ctrl[0], 2 * cy - last_ctrl[1]
|
||||||
|
else:
|
||||||
|
x1, y1 = cx, cy
|
||||||
|
for px, py in flatten_cubic((cx, cy), (x1, y1), (x2, y2), (x, y)):
|
||||||
|
edges.append((cx, cy, px, py))
|
||||||
|
cx, cy = px, py
|
||||||
|
last_ctrl = (x2, y2)
|
||||||
|
elif cmd in ("Z", "z"):
|
||||||
|
line_to(start_x, start_y)
|
||||||
|
cx, cy = start_x, start_y
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unhandled path command {cmd!r}")
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def winding_at(x, y, edges):
|
||||||
|
w = 0
|
||||||
|
for x1, y1, x2, y2 in edges:
|
||||||
|
if y1 == y2:
|
||||||
|
continue
|
||||||
|
if (y1 <= y < y2) or (y2 <= y < y1):
|
||||||
|
t = (y - y1) / (y2 - y1)
|
||||||
|
xi = x1 + t * (x2 - x1)
|
||||||
|
if xi > x:
|
||||||
|
w += 1 if y2 > y1 else -1
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(edges, size, supersample=4):
|
||||||
|
"""Nonzero-winding scanline fill -> size x size alpha coverage (0..255),
|
||||||
|
antialiased via `supersample`x supersampling + box downsample."""
|
||||||
|
hi = size * supersample
|
||||||
|
scale = hi / VIEWBOX
|
||||||
|
scaled = [(x1 * scale, y1 * scale, x2 * scale, y2 * scale) for x1, y1, x2, y2 in edges]
|
||||||
|
coverage = [[0] * hi for _ in range(hi)]
|
||||||
|
for y in range(hi):
|
||||||
|
fy = y + 0.5
|
||||||
|
# crossings for this scanline, sorted, with winding deltas
|
||||||
|
xs = []
|
||||||
|
for x1, y1, x2, y2 in scaled:
|
||||||
|
if y1 == y2:
|
||||||
|
continue
|
||||||
|
if (y1 <= fy < y2) or (y2 <= fy < y1):
|
||||||
|
t = (fy - y1) / (y2 - y1)
|
||||||
|
xi = x1 + t * (x2 - x1)
|
||||||
|
xs.append((xi, 1 if y2 > y1 else -1))
|
||||||
|
xs.sort()
|
||||||
|
w = 0
|
||||||
|
row = coverage[y]
|
||||||
|
for idx in range(len(xs)):
|
||||||
|
xi, d = xs[idx]
|
||||||
|
prev_w = w
|
||||||
|
w += d
|
||||||
|
if prev_w == 0 and w != 0:
|
||||||
|
span_start = xi
|
||||||
|
elif prev_w != 0 and w == 0:
|
||||||
|
x0 = max(0, int(round(span_start)))
|
||||||
|
x1_ = min(hi, int(round(xi)))
|
||||||
|
for px in range(x0, x1_):
|
||||||
|
row[px] = 1
|
||||||
|
# box downsample
|
||||||
|
out = [[0] * size for _ in range(size)]
|
||||||
|
for y in range(size):
|
||||||
|
for x in range(size):
|
||||||
|
s = 0
|
||||||
|
for dy in range(supersample):
|
||||||
|
row = coverage[y * supersample + dy]
|
||||||
|
for dx in range(supersample):
|
||||||
|
s += row[x * supersample + dx]
|
||||||
|
out[y][x] = round(255 * s / (supersample * supersample))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def to_rgba(coverage, size, rgb):
|
||||||
|
r, g, b = rgb
|
||||||
|
buf = bytearray(size * size * 4)
|
||||||
|
for y in range(size):
|
||||||
|
for x in range(size):
|
||||||
|
a = coverage[y][x]
|
||||||
|
o = (y * size + x) * 4
|
||||||
|
buf[o] = r
|
||||||
|
buf[o + 1] = g
|
||||||
|
buf[o + 2] = b
|
||||||
|
buf[o + 3] = a
|
||||||
|
return bytes(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def write_png(path, size, rgba):
|
||||||
|
def chunk(tag, data):
|
||||||
|
return struct.pack(">I", len(data)) + tag + data + struct.pack(
|
||||||
|
">I", zlib.crc32(tag + data) & 0xFFFFFFFF
|
||||||
|
)
|
||||||
|
|
||||||
|
sig = b"\x89PNG\r\n\x1a\n"
|
||||||
|
ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
|
||||||
|
raw = bytearray()
|
||||||
|
stride = size * 4
|
||||||
|
for y in range(size):
|
||||||
|
raw.append(0)
|
||||||
|
raw.extend(rgba[y * stride:(y + 1) * stride])
|
||||||
|
idat = zlib.compress(bytes(raw), 9)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(sig)
|
||||||
|
f.write(chunk(b"IHDR", ihdr))
|
||||||
|
f.write(chunk(b"IDAT", idat))
|
||||||
|
f.write(chunk(b"IEND", b""))
|
||||||
|
|
||||||
|
|
||||||
|
def write_ico(path, frames):
|
||||||
|
"""frames: list of (size, png_bytes)"""
|
||||||
|
count = len(frames)
|
||||||
|
header = struct.pack("<HHH", 0, 1, count)
|
||||||
|
entries = b""
|
||||||
|
blob = b""
|
||||||
|
offset = 6 + count * 16
|
||||||
|
for size, png_bytes in frames:
|
||||||
|
wb = size if size < 256 else 0
|
||||||
|
hb = size if size < 256 else 0
|
||||||
|
entries += struct.pack("<BBBBHHII", wb, hb, 0, 0, 1, 32, len(png_bytes), offset)
|
||||||
|
blob += png_bytes
|
||||||
|
offset += len(png_bytes)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(entries)
|
||||||
|
f.write(blob)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
edges = parse_path(PATH_D)
|
||||||
|
os.makedirs(ASSETS, exist_ok=True)
|
||||||
|
|
||||||
|
green = (0, 255, 136) # ATK accent green #00ff88
|
||||||
|
black = (0, 0, 0)
|
||||||
|
|
||||||
|
sizes = [16, 32]
|
||||||
|
green_pngs = {}
|
||||||
|
for size in sizes:
|
||||||
|
cov = rasterize(edges, size)
|
||||||
|
rgba = to_rgba(cov, size, green)
|
||||||
|
green_pngs[size] = rgba
|
||||||
|
write_png(os.path.join(ASSETS, f"_tray_preview_{size}.png"), size, rgba)
|
||||||
|
|
||||||
|
# Primary tray icon (32px, colored) for Windows/Linux.
|
||||||
|
write_png(os.path.join(ASSETS, "tray_icon.png"), 32, green_pngs[32])
|
||||||
|
|
||||||
|
# macOS template icon: solid black silhouette, alpha only. macOS recolors
|
||||||
|
# this automatically for the light/dark menu bar.
|
||||||
|
cov32 = rasterize(edges, 32)
|
||||||
|
tmpl_rgba = to_rgba(cov32, 32, black)
|
||||||
|
write_png(os.path.join(ASSETS, "tray_icon_template.png"), 32, tmpl_rgba)
|
||||||
|
|
||||||
|
# Windows .ico, 16 + 32px, PNG-compressed frames (Vista+ format).
|
||||||
|
ico_frames = []
|
||||||
|
for size in sizes:
|
||||||
|
tmp_path = os.path.join(ASSETS, f"_ico_tmp_{size}.png")
|
||||||
|
write_png(tmp_path, size, green_pngs[size])
|
||||||
|
with open(tmp_path, "rb") as f:
|
||||||
|
ico_frames.append((size, f.read()))
|
||||||
|
os.remove(tmp_path)
|
||||||
|
write_ico(os.path.join(ASSETS, "tray_icon.ico"), ico_frames)
|
||||||
|
|
||||||
|
# Linux tray icon: a StatusNotifierItem IconName resolved via an app-owned
|
||||||
|
# IconThemePath (not the user's ~/.local/share/icons - no cache to
|
||||||
|
# invalidate). Convention and fill colour (#bebebe) copied directly from
|
||||||
|
# /opt/frequency/icons/frequency-tray-symbolic.svg, a working reference on
|
||||||
|
# this exact machine: GNOME Shell recolors any such named symbolic icon to
|
||||||
|
# monochrome at render time regardless of source colour, so the exact
|
||||||
|
# shade doesn't matter - what matters is shipping a NAME the shell resolves
|
||||||
|
# itself rather than raw pixmap bytes (which is what breaks on GNOME's
|
||||||
|
# AppIndicator extension per Cascade's README).
|
||||||
|
svg = (
|
||||||
|
f'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" '
|
||||||
|
f'viewBox="0 0 {VIEWBOX} {VIEWBOX}"><path d="{PATH_D}" fill="#bebebe"/></svg>\n'
|
||||||
|
)
|
||||||
|
with open(os.path.join(ASSETS, "atk-tray-symbolic.svg"), "w") as f:
|
||||||
|
f.write(svg)
|
||||||
|
|
||||||
|
print("Wrote tray_icon.png, tray_icon_template.png, tray_icon.ico, atk-tray-symbolic.svg")
|
||||||
|
print("Preview files: assets/_tray_preview_16.png, assets/_tray_preview_32.png (delete after checking)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
tray_darwin.go
Normal file
10
tray_darwin.go
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// No tray implementation on macOS yet - only verified on Linux (see
|
||||||
|
// tray_linux.go) against this project's actual dev/test machine. ATK runs
|
||||||
|
// fine without it; the frontend's close dialog just won't offer "Minimize to
|
||||||
|
// tray" here (TrayAvailable() is false).
|
||||||
|
|
||||||
|
func initTray(a *App) {}
|
||||||
|
func closeTray() {}
|
||||||
|
func trayAvailable() bool { return false }
|
||||||
339
tray_linux.go
Normal file
339
tray_linux.go
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/godbus/dbus/v5/introspect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Linux tray, implemented as a minimal StatusNotifierItem (+ dbusmenu) server
|
||||||
|
// directly over D-Bus. Ported from a working reference on this same machine:
|
||||||
|
// Frequency's gui.py `_Tray` class (/opt/frequency/gui.py). GTK/Wails have no
|
||||||
|
// portable tray API, and the common Go systray wrappers pull in libappindicator
|
||||||
|
// or ship raw icon pixmap bytes - the latter is exactly what breaks GNOME's
|
||||||
|
// AppIndicator extension (crashes with "can't access property 'clone'",
|
||||||
|
// documented in Cascade's README). Using an IconName resolved via our own
|
||||||
|
// IconThemePath (not the user's ~/.local/share/icons - nothing to cache-
|
||||||
|
// invalidate) sidesteps that entirely, same as both reference apps do it.
|
||||||
|
|
||||||
|
//go:embed assets/atk-tray-symbolic.svg
|
||||||
|
var trayIconSVG []byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
trayIconName = "atk-tray-symbolic"
|
||||||
|
sniPath = dbus.ObjectPath("/StatusNotifierItem")
|
||||||
|
menuPath = dbus.ObjectPath("/MenuBar")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
trayMu sync.Mutex
|
||||||
|
trayState *linuxTray
|
||||||
|
)
|
||||||
|
|
||||||
|
type linuxTray struct {
|
||||||
|
app *App
|
||||||
|
conn *dbus.Conn
|
||||||
|
iconDir string
|
||||||
|
ok bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func initTray(a *App) {
|
||||||
|
trayMu.Lock()
|
||||||
|
defer trayMu.Unlock()
|
||||||
|
if trayState != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := &linuxTray{app: a}
|
||||||
|
if err := t.start(); err != nil {
|
||||||
|
// Non-fatal: no session bus, no StatusNotifierWatcher (extension not
|
||||||
|
// installed/enabled), etc. ATK just runs without a tray icon.
|
||||||
|
t.ok = false
|
||||||
|
}
|
||||||
|
trayState = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeTray() {
|
||||||
|
trayMu.Lock()
|
||||||
|
defer trayMu.Unlock()
|
||||||
|
if trayState == nil || trayState.conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = trayState.conn.Close()
|
||||||
|
trayState = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trayAvailable() bool {
|
||||||
|
trayMu.Lock()
|
||||||
|
defer trayMu.Unlock()
|
||||||
|
return trayState != nil && trayState.ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *linuxTray) start() error {
|
||||||
|
if err := t.writeIcon(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := dbus.ConnectSessionBus()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.conn = conn
|
||||||
|
|
||||||
|
if err := t.exportSNI(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := t.exportMenu(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid())
|
||||||
|
reply, err := conn.RequestName(name, dbus.NameFlagDoNotQueue)
|
||||||
|
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
|
||||||
|
return fmt.Errorf("could not own tray bus name")
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher := conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
|
||||||
|
call := watcher.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, name)
|
||||||
|
if call.Err != nil {
|
||||||
|
// No watcher running (AppIndicator extension off, or a DE without one).
|
||||||
|
return call.Err
|
||||||
|
}
|
||||||
|
t.ok = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeIcon materialises the embedded SVG to an app-owned directory and
|
||||||
|
// advertises that directory via IconThemePath, rather than installing into
|
||||||
|
// the user's shared ~/.local/share/icons (no icon-cache/gtk-update step
|
||||||
|
// needed - the same approach Frequency uses via its bundled icons/ dir).
|
||||||
|
func (t *linuxTray) writeIcon() error {
|
||||||
|
cacheDir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
cacheDir = os.TempDir()
|
||||||
|
}
|
||||||
|
dir := filepath.Join(cacheDir, "atk", "tray-icons")
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, trayIconName+".svg")
|
||||||
|
if err := os.WriteFile(path, trayIconSVG, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.iconDir = dir
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- org.kde.StatusNotifierItem -------------------------------------------
|
||||||
|
|
||||||
|
const sniIntrospectXML = `
|
||||||
|
<interface name="org.kde.StatusNotifierItem">
|
||||||
|
<property name="Category" type="s" access="read"/>
|
||||||
|
<property name="Id" type="s" access="read"/>
|
||||||
|
<property name="Title" type="s" access="read"/>
|
||||||
|
<property name="Status" type="s" access="read"/>
|
||||||
|
<property name="IconName" type="s" access="read"/>
|
||||||
|
<property name="IconThemePath" type="s" access="read"/>
|
||||||
|
<property name="Menu" type="o" access="read"/>
|
||||||
|
<property name="ItemIsMenu" type="b" access="read"/>
|
||||||
|
<method name="Activate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||||
|
<method name="SecondaryActivate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||||
|
<method name="ContextMenu"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
||||||
|
<method name="Scroll"><arg type="i" direction="in"/><arg type="s" direction="in"/></method>
|
||||||
|
</interface>`
|
||||||
|
|
||||||
|
type sniHandler struct{ t *linuxTray }
|
||||||
|
|
||||||
|
func (h *sniHandler) Activate(x, y int32) *dbus.Error {
|
||||||
|
h.t.app.ToggleWindow()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (h *sniHandler) SecondaryActivate(x, y int32) *dbus.Error {
|
||||||
|
h.t.app.ToggleWindow()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (h *sniHandler) ContextMenu(x, y int32) *dbus.Error { return nil }
|
||||||
|
func (h *sniHandler) Scroll(delta int32, orientation string) *dbus.Error { return nil }
|
||||||
|
|
||||||
|
func (h *sniHandler) sniProps() map[string]dbus.Variant {
|
||||||
|
return map[string]dbus.Variant{
|
||||||
|
"Category": dbus.MakeVariant("ApplicationStatus"),
|
||||||
|
"Id": dbus.MakeVariant("atk"),
|
||||||
|
"Title": dbus.MakeVariant("ATK"),
|
||||||
|
"Status": dbus.MakeVariant("Active"),
|
||||||
|
"IconName": dbus.MakeVariant(trayIconName),
|
||||||
|
"IconThemePath": dbus.MakeVariant(h.t.iconDir),
|
||||||
|
"Menu": dbus.MakeVariant(menuPath),
|
||||||
|
"ItemIsMenu": dbus.MakeVariant(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *sniHandler) Get(iface, propName string) (dbus.Variant, *dbus.Error) {
|
||||||
|
v, ok := h.sniProps()[propName]
|
||||||
|
if !ok {
|
||||||
|
return dbus.Variant{}, dbus.NewError("org.freedesktop.DBus.Error.UnknownProperty", nil)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
func (h *sniHandler) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) {
|
||||||
|
return h.sniProps(), nil
|
||||||
|
}
|
||||||
|
func (h *sniHandler) Set(iface, propName string, value dbus.Variant) *dbus.Error {
|
||||||
|
return dbus.NewError("org.freedesktop.DBus.Error.PropertyReadOnly", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *linuxTray) exportSNI() error {
|
||||||
|
h := &sniHandler{t: t}
|
||||||
|
if err := t.conn.Export(h, sniPath, "org.kde.StatusNotifierItem"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := t.conn.Export(h, sniPath, "org.freedesktop.DBus.Properties"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node := `<node>` + sniIntrospectXML + `</node>`
|
||||||
|
return t.conn.Export(introspect.Introspectable(node), sniPath, "org.freedesktop.DBus.Introspectable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- com.canonical.dbusmenu -------------------------------------------
|
||||||
|
|
||||||
|
const menuIntrospectXML = `
|
||||||
|
<interface name="com.canonical.dbusmenu">
|
||||||
|
<property name="Version" type="u" access="read"/>
|
||||||
|
<property name="Status" type="s" access="read"/>
|
||||||
|
<method name="GetLayout">
|
||||||
|
<arg type="i" direction="in"/><arg type="i" direction="in"/><arg type="as" direction="in"/>
|
||||||
|
<arg type="u" direction="out"/><arg type="(ia{sv}av)" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetGroupProperties">
|
||||||
|
<arg type="ai" direction="in"/><arg type="as" direction="in"/>
|
||||||
|
<arg type="a(ia{sv})" direction="out"/>
|
||||||
|
</method>
|
||||||
|
<method name="Event">
|
||||||
|
<arg type="i" direction="in"/><arg type="s" direction="in"/>
|
||||||
|
<arg type="v" direction="in"/><arg type="u" direction="in"/>
|
||||||
|
</method>
|
||||||
|
<method name="AboutToShow"><arg type="i" direction="in"/><arg type="b" direction="out"/></method>
|
||||||
|
</interface>`
|
||||||
|
|
||||||
|
// menuNode mirrors dbusmenu's recursive (ia{sv}av) layout struct: id,
|
||||||
|
// properties, children (each child a variant wrapping another menuNode).
|
||||||
|
type menuNode struct {
|
||||||
|
ID int32
|
||||||
|
Props map[string]dbus.Variant
|
||||||
|
Children []dbus.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupProps struct {
|
||||||
|
ID int32
|
||||||
|
Props map[string]dbus.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
// menuItem ids: 0 is reserved for the menu root (GetLayout's implicit parent),
|
||||||
|
// so item/separator ids start at 1 and must all stay distinct from it and
|
||||||
|
// from each other.
|
||||||
|
type menuItemDef struct {
|
||||||
|
id int32
|
||||||
|
label string // empty => separator
|
||||||
|
}
|
||||||
|
|
||||||
|
func menuItems() []menuItemDef {
|
||||||
|
return []menuItemDef{
|
||||||
|
{1, "Show / Hide ATK"},
|
||||||
|
{2, ""}, // separator
|
||||||
|
{3, "Quit ATK"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func menuItemProps(item menuItemDef) map[string]dbus.Variant {
|
||||||
|
if item.label == "" {
|
||||||
|
return map[string]dbus.Variant{"type": dbus.MakeVariant("separator")}
|
||||||
|
}
|
||||||
|
return map[string]dbus.Variant{
|
||||||
|
"label": dbus.MakeVariant(item.label),
|
||||||
|
"enabled": dbus.MakeVariant(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type menuHandler struct{ t *linuxTray }
|
||||||
|
|
||||||
|
func (h *menuHandler) GetLayout(parentID, recursionDepth int32, propertyNames []string) (uint32, menuNode, *dbus.Error) {
|
||||||
|
var children []dbus.Variant
|
||||||
|
for _, item := range menuItems() {
|
||||||
|
children = append(children, dbus.MakeVariant(menuNode{
|
||||||
|
ID: item.id,
|
||||||
|
Props: menuItemProps(item),
|
||||||
|
Children: nil,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
root := menuNode{
|
||||||
|
ID: 0,
|
||||||
|
Props: map[string]dbus.Variant{"children-display": dbus.MakeVariant("submenu")},
|
||||||
|
Children: children,
|
||||||
|
}
|
||||||
|
return 1, root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *menuHandler) GetGroupProperties(ids []int32, propertyNames []string) ([]groupProps, *dbus.Error) {
|
||||||
|
wanted := make(map[int32]bool, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
wanted[id] = true
|
||||||
|
}
|
||||||
|
var out []groupProps
|
||||||
|
for _, item := range menuItems() {
|
||||||
|
if wanted[item.id] {
|
||||||
|
out = append(out, groupProps{ID: item.id, Props: menuItemProps(item)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *menuHandler) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) *dbus.Error {
|
||||||
|
if eventID != "clicked" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch id {
|
||||||
|
case 1:
|
||||||
|
h.t.app.ToggleWindow()
|
||||||
|
case 3:
|
||||||
|
h.t.app.QuitApp()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *menuHandler) AboutToShow(id int32) (bool, *dbus.Error) { return false, nil }
|
||||||
|
|
||||||
|
func (h *menuHandler) menuProps() map[string]dbus.Variant {
|
||||||
|
return map[string]dbus.Variant{
|
||||||
|
"Version": dbus.MakeVariant(uint32(3)),
|
||||||
|
"Status": dbus.MakeVariant("normal"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (h *menuHandler) Get(iface, propName string) (dbus.Variant, *dbus.Error) {
|
||||||
|
v, ok := h.menuProps()[propName]
|
||||||
|
if !ok {
|
||||||
|
return dbus.Variant{}, dbus.NewError("org.freedesktop.DBus.Error.UnknownProperty", nil)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
func (h *menuHandler) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) {
|
||||||
|
return h.menuProps(), nil
|
||||||
|
}
|
||||||
|
func (h *menuHandler) Set(iface, propName string, value dbus.Variant) *dbus.Error {
|
||||||
|
return dbus.NewError("org.freedesktop.DBus.Error.PropertyReadOnly", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *linuxTray) exportMenu() error {
|
||||||
|
h := &menuHandler{t: t}
|
||||||
|
if err := t.conn.Export(h, menuPath, "com.canonical.dbusmenu"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := t.conn.Export(h, menuPath, "org.freedesktop.DBus.Properties"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node := `<node>` + menuIntrospectXML + `</node>`
|
||||||
|
return t.conn.Export(introspect.Introspectable(node), menuPath, "org.freedesktop.DBus.Introspectable")
|
||||||
|
}
|
||||||
10
tray_windows.go
Normal file
10
tray_windows.go
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// No tray implementation on Windows yet - only verified on Linux (see
|
||||||
|
// tray_linux.go) against this project's actual dev/test machine. ATK runs
|
||||||
|
// fine without it; the frontend's close dialog just won't offer "Minimize to
|
||||||
|
// tray" here (TrayAvailable() is false).
|
||||||
|
|
||||||
|
func initTray(a *App) {}
|
||||||
|
func closeTray() {}
|
||||||
|
func trayAvailable() bool { return false }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue