Upload 112 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +36 -36
- .gitignore +7 -7
- README.md +98 -98
- deno.json +19 -19
- deno.lock +1128 -0
- docs/ARCHITECTURE_PIVOT.md +43 -43
- docs/MUSE_SRS_v3.md +205 -205
- docs/MUSE_SRS_v4.md +170 -170
- index.html +12 -12
- package.json +36 -36
- src-tauri/Cargo.lock +0 -0
- src-tauri/Cargo.toml +2 -0
- src-tauri/build.rs +3 -3
- src-tauri/capabilities/default.json +29 -29
- src-tauri/gen/schemas/acl-manifests.json +0 -0
- src-tauri/gen/schemas/capabilities.json +1 -0
- src-tauri/gen/schemas/desktop-schema.json +0 -0
- src-tauri/gen/schemas/windows-schema.json +0 -0
- src-tauri/icons/README.md +1 -1
- src-tauri/migrations/001_phase0_init.sql +21 -21
- src-tauri/migrations/002_phase3_tables.sql +94 -94
- src-tauri/resources/filters/annoyances_mini.txt +20 -20
- src-tauri/resources/filters/easylist_mini.txt +51 -51
- src-tauri/resources/filters/easyprivacy_mini.txt +28 -28
- src-tauri/resources/scriptlets/muse_ubo_compatible_scriptlets.js +105 -105
- src-tauri/resources/scripts/adblock_layer1.js +141 -141
- src-tauri/resources/scripts/autofill_suppress.js +71 -71
- src-tauri/resources/scripts/canvas_noise.js +42 -42
- src-tauri/resources/scripts/cookie_consent.js +72 -72
- src-tauri/resources/scripts/hover_overlay.js +144 -144
- src-tauri/resources/scripts/vault_detector.js +159 -159
- src-tauri/resources/scripts/video_ad_scriptlets.js +314 -314
- src-tauri/resources/scripts/webrtc_protect.js +24 -24
- src-tauri/src/adblock/commands.rs +64 -64
- src-tauri/src/adblock/engine.rs +154 -154
- src-tauri/src/adblock/mod.rs +4 -4
- src-tauri/src/adblock/scripts.rs +71 -71
- src-tauri/src/adblock/updater.rs +103 -103
- src-tauri/src/board.rs +222 -222
- src-tauri/src/browser/autofill.rs +13 -13
- src-tauri/src/browser/capture.rs +93 -93
- src-tauri/src/browser/commands.rs +272 -225
- src-tauri/src/browser/context_menu.rs +23 -23
- src-tauri/src/browser/layout.rs +49 -49
- src-tauri/src/browser/mod.rs +9 -9
- src-tauri/src/browser/navigation.rs +36 -36
- src-tauri/src/browser/tab_manager.rs +102 -102
- src-tauri/src/color_tools.rs +108 -108
- src-tauri/src/credentials.rs +31 -31
- src-tauri/src/downloads.rs +75 -75
.gitattributes
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
-
uiprototype2/doc/Refstudio_SRS_v1.0.pdf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
uiprototype2/doc/Refstudio_SRS_v1.0.pdf filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
node_modules/
|
| 2 |
-
dist/
|
| 3 |
-
src-tauri/target/
|
| 4 |
-
src-tauri/gen/
|
| 5 |
-
.DS_Store
|
| 6 |
-
*.log
|
| 7 |
-
.env
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
src-tauri/target/
|
| 4 |
+
src-tauri/gen/
|
| 5 |
+
.DS_Store
|
| 6 |
+
*.log
|
| 7 |
+
.env
|
README.md
CHANGED
|
@@ -1,98 +1,98 @@
|
|
| 1 |
-
---
|
| 2 |
-
tags:
|
| 3 |
-
- ml-intern
|
| 4 |
-
---
|
| 5 |
-
# MUSE Alpha — Creative Browser
|
| 6 |
-
|
| 7 |
-
Tauri v2 + Rust + SolidJS · Phase 0–4 implementation.
|
| 8 |
-
|
| 9 |
-
> *The browser that understands that getting the work done matters more than any single feature.*
|
| 10 |
-
|
| 11 |
-
## Download
|
| 12 |
-
|
| 13 |
-
**Latest project:** [`artifacts/musealpha-production.zip`](artifacts/musealpha-production.zip)
|
| 14 |
-
|
| 15 |
-
## Run
|
| 16 |
-
|
| 17 |
-
```bash
|
| 18 |
-
deno install
|
| 19 |
-
deno task tauri dev
|
| 20 |
-
```
|
| 21 |
-
|
| 22 |
-
## Architecture
|
| 23 |
-
|
| 24 |
-
- **Shell:** SolidJS in main WebView (titlebar, sidebar, URL bar, library, board, study mode)
|
| 25 |
-
- **Browser tabs:** Tauri child WebViews via `window.add_child()` (unstable feature)
|
| 26 |
-
- **Ad-block:** `adblock-rust 0.12` + JS injection scripts (Layer 0-3)
|
| 27 |
-
- **Privacy:** WebRTC protection, canvas fingerprint noise, cookie consent auto-deny
|
| 28 |
-
- **Data:** SQLite (via tauri-plugin-sql) + JSON persistence + Stronghold vault
|
| 29 |
-
- **Design:** 8 production-polished theme presets, tokenized design system
|
| 30 |
-
|
| 31 |
-
## Implemented Phases
|
| 32 |
-
|
| 33 |
-
### Phase 0 — Foundation ✓
|
| 34 |
-
Custom titlebar, 8-theme system, onboarding, settings persistence, SQLite migrations.
|
| 35 |
-
|
| 36 |
-
### Phase 1 — Browser ✓
|
| 37 |
-
Multi-tab via child WebViews, vertical tab sidebar, URL navigation, back/forward/reload/zoom.
|
| 38 |
-
|
| 39 |
-
### Phase 2 — Ad-Block & Privacy ✓
|
| 40 |
-
adblock-rust engine, bundled filter lists, daily updater, JS injection scripts, cosmetic filtering, navigation blocking, per-domain shield controls, HTTPS-first, Stronghold vault.
|
| 41 |
-
|
| 42 |
-
### Phase 3 — Library & Board ✓
|
| 43 |
-
Library (add by URL, Blake3 hash, dimensions, palette extraction, grid, detail, search), Board (infinite canvas, pan/zoom, image/note/palette cards, drag from library), hover overlay, downloads, sessions.
|
| 44 |
-
|
| 45 |
-
### Phase 4 — Artist Features ✓
|
| 46 |
-
Quick Study Mode (timer, opacity, flip, notes), Command Palette (Ctrl+K), Session Manager, Color Export (HEX/CSS/GPL/ASE/Procreate), color search by hue.
|
| 47 |
-
|
| 48 |
-
## Design System
|
| 49 |
-
|
| 50 |
-
- **Tokens:** `src/styles/tokens.css` — spacing, radius, motion, z-index, per-theme colors
|
| 51 |
-
- **Primitives:** `src/styles/primitives.css` — buttons, inputs, cards, badges, dividers
|
| 52 |
-
- **Themes:** Dusk, Parchment, Midnight, Studio, Moss, Rose, Obsidian, Linen
|
| 53 |
-
- **State:** `src/store/appStore.ts` — centralized SolidJS store
|
| 54 |
-
|
| 55 |
-
## Keyboard Shortcuts
|
| 56 |
-
|
| 57 |
-
| Shortcut | Action |
|
| 58 |
-
|---|---|
|
| 59 |
-
| `Ctrl+K` | Command Palette |
|
| 60 |
-
| `Ctrl+Shift+Y` | Quick Study Mode |
|
| 61 |
-
| `Escape` | Close palette/study |
|
| 62 |
-
|
| 63 |
-
## Docs
|
| 64 |
-
|
| 65 |
-
- [`docs/MUSE_SRS_v3.md`](docs/MUSE_SRS_v3.md) — Product requirements
|
| 66 |
-
- [`docs/MUSE_SRS_v4.md`](docs/MUSE_SRS_v4.md) — Implementation guide
|
| 67 |
-
|
| 68 |
-
## Known Limitations
|
| 69 |
-
|
| 70 |
-
- Library/Board use JSON persistence, not full SQLite FTS5 yet
|
| 71 |
-
- Password manager UI not implemented (vault backend ready)
|
| 72 |
-
- Board annotations/overlays/export not implemented
|
| 73 |
-
- Remote hover overlay IPC requires broad capabilities
|
| 74 |
-
- No test suite
|
| 75 |
-
|
| 76 |
-
---
|
| 77 |
-
|
| 78 |
-
Generated by [ML Intern](https://github.com/huggingface/ml-intern).
|
| 79 |
-
|
| 80 |
-
<!-- ml-intern-provenance -->
|
| 81 |
-
## Generated by ML Intern
|
| 82 |
-
|
| 83 |
-
This model repository was generated by [ML Intern](https://github.com/huggingface/ml-intern), an agent for machine learning research and development on the Hugging Face Hub.
|
| 84 |
-
|
| 85 |
-
- Try ML Intern: https://smolagents-ml-intern.hf.space
|
| 86 |
-
- Source code: https://github.com/huggingface/ml-intern
|
| 87 |
-
|
| 88 |
-
## Usage
|
| 89 |
-
|
| 90 |
-
```python
|
| 91 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 92 |
-
|
| 93 |
-
model_id = 'asdf98/musealpha'
|
| 94 |
-
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 95 |
-
model = AutoModelForCausalLM.from_pretrained(model_id)
|
| 96 |
-
```
|
| 97 |
-
|
| 98 |
-
For non-causal architectures, replace `AutoModelForCausalLM` with the appropriate `AutoModel` class.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags:
|
| 3 |
+
- ml-intern
|
| 4 |
+
---
|
| 5 |
+
# MUSE Alpha — Creative Browser
|
| 6 |
+
|
| 7 |
+
Tauri v2 + Rust + SolidJS · Phase 0–4 implementation.
|
| 8 |
+
|
| 9 |
+
> *The browser that understands that getting the work done matters more than any single feature.*
|
| 10 |
+
|
| 11 |
+
## Download
|
| 12 |
+
|
| 13 |
+
**Latest project:** [`artifacts/musealpha-production.zip`](artifacts/musealpha-production.zip)
|
| 14 |
+
|
| 15 |
+
## Run
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
deno install
|
| 19 |
+
deno task tauri dev
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Architecture
|
| 23 |
+
|
| 24 |
+
- **Shell:** SolidJS in main WebView (titlebar, sidebar, URL bar, library, board, study mode)
|
| 25 |
+
- **Browser tabs:** Tauri child WebViews via `window.add_child()` (unstable feature)
|
| 26 |
+
- **Ad-block:** `adblock-rust 0.12` + JS injection scripts (Layer 0-3)
|
| 27 |
+
- **Privacy:** WebRTC protection, canvas fingerprint noise, cookie consent auto-deny
|
| 28 |
+
- **Data:** SQLite (via tauri-plugin-sql) + JSON persistence + Stronghold vault
|
| 29 |
+
- **Design:** 8 production-polished theme presets, tokenized design system
|
| 30 |
+
|
| 31 |
+
## Implemented Phases
|
| 32 |
+
|
| 33 |
+
### Phase 0 — Foundation ✓
|
| 34 |
+
Custom titlebar, 8-theme system, onboarding, settings persistence, SQLite migrations.
|
| 35 |
+
|
| 36 |
+
### Phase 1 — Browser ✓
|
| 37 |
+
Multi-tab via child WebViews, vertical tab sidebar, URL navigation, back/forward/reload/zoom.
|
| 38 |
+
|
| 39 |
+
### Phase 2 — Ad-Block & Privacy ✓
|
| 40 |
+
adblock-rust engine, bundled filter lists, daily updater, JS injection scripts, cosmetic filtering, navigation blocking, per-domain shield controls, HTTPS-first, Stronghold vault.
|
| 41 |
+
|
| 42 |
+
### Phase 3 — Library & Board ✓
|
| 43 |
+
Library (add by URL, Blake3 hash, dimensions, palette extraction, grid, detail, search), Board (infinite canvas, pan/zoom, image/note/palette cards, drag from library), hover overlay, downloads, sessions.
|
| 44 |
+
|
| 45 |
+
### Phase 4 — Artist Features ✓
|
| 46 |
+
Quick Study Mode (timer, opacity, flip, notes), Command Palette (Ctrl+K), Session Manager, Color Export (HEX/CSS/GPL/ASE/Procreate), color search by hue.
|
| 47 |
+
|
| 48 |
+
## Design System
|
| 49 |
+
|
| 50 |
+
- **Tokens:** `src/styles/tokens.css` — spacing, radius, motion, z-index, per-theme colors
|
| 51 |
+
- **Primitives:** `src/styles/primitives.css` — buttons, inputs, cards, badges, dividers
|
| 52 |
+
- **Themes:** Dusk, Parchment, Midnight, Studio, Moss, Rose, Obsidian, Linen
|
| 53 |
+
- **State:** `src/store/appStore.ts` — centralized SolidJS store
|
| 54 |
+
|
| 55 |
+
## Keyboard Shortcuts
|
| 56 |
+
|
| 57 |
+
| Shortcut | Action |
|
| 58 |
+
|---|---|
|
| 59 |
+
| `Ctrl+K` | Command Palette |
|
| 60 |
+
| `Ctrl+Shift+Y` | Quick Study Mode |
|
| 61 |
+
| `Escape` | Close palette/study |
|
| 62 |
+
|
| 63 |
+
## Docs
|
| 64 |
+
|
| 65 |
+
- [`docs/MUSE_SRS_v3.md`](docs/MUSE_SRS_v3.md) — Product requirements
|
| 66 |
+
- [`docs/MUSE_SRS_v4.md`](docs/MUSE_SRS_v4.md) — Implementation guide
|
| 67 |
+
|
| 68 |
+
## Known Limitations
|
| 69 |
+
|
| 70 |
+
- Library/Board use JSON persistence, not full SQLite FTS5 yet
|
| 71 |
+
- Password manager UI not implemented (vault backend ready)
|
| 72 |
+
- Board annotations/overlays/export not implemented
|
| 73 |
+
- Remote hover overlay IPC requires broad capabilities
|
| 74 |
+
- No test suite
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
Generated by [ML Intern](https://github.com/huggingface/ml-intern).
|
| 79 |
+
|
| 80 |
+
<!-- ml-intern-provenance -->
|
| 81 |
+
## Generated by ML Intern
|
| 82 |
+
|
| 83 |
+
This model repository was generated by [ML Intern](https://github.com/huggingface/ml-intern), an agent for machine learning research and development on the Hugging Face Hub.
|
| 84 |
+
|
| 85 |
+
- Try ML Intern: https://smolagents-ml-intern.hf.space
|
| 86 |
+
- Source code: https://github.com/huggingface/ml-intern
|
| 87 |
+
|
| 88 |
+
## Usage
|
| 89 |
+
|
| 90 |
+
```python
|
| 91 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 92 |
+
|
| 93 |
+
model_id = 'asdf98/musealpha'
|
| 94 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 95 |
+
model = AutoModelForCausalLM.from_pretrained(model_id)
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
For non-causal architectures, replace `AutoModelForCausalLM` with the appropriate `AutoModel` class.
|
deno.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
{
|
| 2 |
-
"tasks": {
|
| 3 |
-
"dev": "deno run -A npm:vite --port 1420 --host",
|
| 4 |
-
"build": "deno run -A npm:vite build",
|
| 5 |
-
"preview": "deno run -A npm:vite preview",
|
| 6 |
-
"tauri": "deno run -A npm:@tauri-apps/cli"
|
| 7 |
-
},
|
| 8 |
-
"imports": {
|
| 9 |
-
"react": "npm:react@^19",
|
| 10 |
-
"react-dom": "npm:react-dom@^19",
|
| 11 |
-
"react/jsx-runtime": "npm:react@^19/jsx-runtime"
|
| 12 |
-
},
|
| 13 |
-
"compilerOptions": {
|
| 14 |
-
"jsx": "react-jsx",
|
| 15 |
-
"jsxImportSource": "react",
|
| 16 |
-
"lib": ["dom", "dom.iterable", "esnext"]
|
| 17 |
-
},
|
| 18 |
-
"nodeModulesDir": "auto"
|
| 19 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tasks": {
|
| 3 |
+
"dev": "deno run -A npm:vite --port 1420 --host",
|
| 4 |
+
"build": "deno run -A npm:vite build",
|
| 5 |
+
"preview": "deno run -A npm:vite preview",
|
| 6 |
+
"tauri": "deno run -A npm:@tauri-apps/cli"
|
| 7 |
+
},
|
| 8 |
+
"imports": {
|
| 9 |
+
"react": "npm:react@^19",
|
| 10 |
+
"react-dom": "npm:react-dom@^19",
|
| 11 |
+
"react/jsx-runtime": "npm:react@^19/jsx-runtime"
|
| 12 |
+
},
|
| 13 |
+
"compilerOptions": {
|
| 14 |
+
"jsx": "react-jsx",
|
| 15 |
+
"jsxImportSource": "react",
|
| 16 |
+
"lib": ["dom", "dom.iterable", "esnext"]
|
| 17 |
+
},
|
| 18 |
+
"nodeModulesDir": "auto"
|
| 19 |
+
}
|
deno.lock
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "5",
|
| 3 |
+
"specifiers": {
|
| 4 |
+
"npm:@tailwindcss/vite@4": "4.3.0_vite@6.4.2__picomatch@4.0.4",
|
| 5 |
+
"npm:@tauri-apps/api@2": "2.11.0",
|
| 6 |
+
"npm:@tauri-apps/cli@*": "2.11.1",
|
| 7 |
+
"npm:@tauri-apps/cli@2": "2.11.1",
|
| 8 |
+
"npm:@tauri-apps/plugin-clipboard-manager@2": "2.3.2",
|
| 9 |
+
"npm:@tauri-apps/plugin-fs@2": "2.5.1",
|
| 10 |
+
"npm:@tauri-apps/plugin-opener@2": "2.5.3",
|
| 11 |
+
"npm:@tauri-apps/plugin-store@2": "2.4.3",
|
| 12 |
+
"npm:@tauri-apps/plugin-stronghold@2": "2.3.1",
|
| 13 |
+
"npm:@types/react-dom@19": "19.2.3_@types+react@19.2.14",
|
| 14 |
+
"npm:@types/react@19": "19.2.14",
|
| 15 |
+
"npm:@vitejs/plugin-react@^4.3.0": "4.7.0_vite@6.4.2__picomatch@4.0.4_@babel+core@7.29.0",
|
| 16 |
+
"npm:autoprefixer@^10.4.20": "10.5.0_postcss@8.5.14",
|
| 17 |
+
"npm:lucide-react@0.468": "0.468.0_react@19.2.6",
|
| 18 |
+
"npm:motion@12": "12.38.0_react@19.2.6_react-dom@19.2.6__react@19.2.6",
|
| 19 |
+
"npm:postcss@^8.4.49": "8.5.14",
|
| 20 |
+
"npm:react-dom@19": "19.2.6_react@19.2.6",
|
| 21 |
+
"npm:react@19": "19.2.6",
|
| 22 |
+
"npm:tailwindcss@4": "4.3.0",
|
| 23 |
+
"npm:typescript@~5.6.2": "5.6.3",
|
| 24 |
+
"npm:vite@*": "6.4.2_picomatch@4.0.4",
|
| 25 |
+
"npm:vite@6": "6.4.2_picomatch@4.0.4"
|
| 26 |
+
},
|
| 27 |
+
"npm": {
|
| 28 |
+
"@babel/code-frame@7.29.0": {
|
| 29 |
+
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 30 |
+
"dependencies": [
|
| 31 |
+
"@babel/helper-validator-identifier",
|
| 32 |
+
"js-tokens",
|
| 33 |
+
"picocolors"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
"@babel/compat-data@7.29.3": {
|
| 37 |
+
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="
|
| 38 |
+
},
|
| 39 |
+
"@babel/core@7.29.0": {
|
| 40 |
+
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
| 41 |
+
"dependencies": [
|
| 42 |
+
"@babel/code-frame",
|
| 43 |
+
"@babel/generator",
|
| 44 |
+
"@babel/helper-compilation-targets",
|
| 45 |
+
"@babel/helper-module-transforms",
|
| 46 |
+
"@babel/helpers",
|
| 47 |
+
"@babel/parser",
|
| 48 |
+
"@babel/template",
|
| 49 |
+
"@babel/traverse",
|
| 50 |
+
"@babel/types",
|
| 51 |
+
"@jridgewell/remapping",
|
| 52 |
+
"convert-source-map",
|
| 53 |
+
"debug",
|
| 54 |
+
"gensync",
|
| 55 |
+
"json5",
|
| 56 |
+
"semver"
|
| 57 |
+
]
|
| 58 |
+
},
|
| 59 |
+
"@babel/generator@7.29.1": {
|
| 60 |
+
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 61 |
+
"dependencies": [
|
| 62 |
+
"@babel/parser",
|
| 63 |
+
"@babel/types",
|
| 64 |
+
"@jridgewell/gen-mapping",
|
| 65 |
+
"@jridgewell/trace-mapping",
|
| 66 |
+
"jsesc"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
"@babel/helper-compilation-targets@7.28.6": {
|
| 70 |
+
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
| 71 |
+
"dependencies": [
|
| 72 |
+
"@babel/compat-data",
|
| 73 |
+
"@babel/helper-validator-option",
|
| 74 |
+
"browserslist",
|
| 75 |
+
"lru-cache",
|
| 76 |
+
"semver"
|
| 77 |
+
]
|
| 78 |
+
},
|
| 79 |
+
"@babel/helper-globals@7.28.0": {
|
| 80 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="
|
| 81 |
+
},
|
| 82 |
+
"@babel/helper-module-imports@7.28.6": {
|
| 83 |
+
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 84 |
+
"dependencies": [
|
| 85 |
+
"@babel/traverse",
|
| 86 |
+
"@babel/types"
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
"@babel/helper-module-transforms@7.28.6_@babel+core@7.29.0": {
|
| 90 |
+
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
| 91 |
+
"dependencies": [
|
| 92 |
+
"@babel/core",
|
| 93 |
+
"@babel/helper-module-imports",
|
| 94 |
+
"@babel/helper-validator-identifier",
|
| 95 |
+
"@babel/traverse"
|
| 96 |
+
]
|
| 97 |
+
},
|
| 98 |
+
"@babel/helper-plugin-utils@7.28.6": {
|
| 99 |
+
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="
|
| 100 |
+
},
|
| 101 |
+
"@babel/helper-string-parser@7.27.1": {
|
| 102 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
|
| 103 |
+
},
|
| 104 |
+
"@babel/helper-validator-identifier@7.28.5": {
|
| 105 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="
|
| 106 |
+
},
|
| 107 |
+
"@babel/helper-validator-option@7.27.1": {
|
| 108 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="
|
| 109 |
+
},
|
| 110 |
+
"@babel/helpers@7.29.2": {
|
| 111 |
+
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
|
| 112 |
+
"dependencies": [
|
| 113 |
+
"@babel/template",
|
| 114 |
+
"@babel/types"
|
| 115 |
+
]
|
| 116 |
+
},
|
| 117 |
+
"@babel/parser@7.29.3": {
|
| 118 |
+
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
|
| 119 |
+
"dependencies": [
|
| 120 |
+
"@babel/types"
|
| 121 |
+
],
|
| 122 |
+
"bin": true
|
| 123 |
+
},
|
| 124 |
+
"@babel/plugin-transform-react-jsx-self@7.27.1_@babel+core@7.29.0": {
|
| 125 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 126 |
+
"dependencies": [
|
| 127 |
+
"@babel/core",
|
| 128 |
+
"@babel/helper-plugin-utils"
|
| 129 |
+
]
|
| 130 |
+
},
|
| 131 |
+
"@babel/plugin-transform-react-jsx-source@7.27.1_@babel+core@7.29.0": {
|
| 132 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 133 |
+
"dependencies": [
|
| 134 |
+
"@babel/core",
|
| 135 |
+
"@babel/helper-plugin-utils"
|
| 136 |
+
]
|
| 137 |
+
},
|
| 138 |
+
"@babel/template@7.28.6": {
|
| 139 |
+
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 140 |
+
"dependencies": [
|
| 141 |
+
"@babel/code-frame",
|
| 142 |
+
"@babel/parser",
|
| 143 |
+
"@babel/types"
|
| 144 |
+
]
|
| 145 |
+
},
|
| 146 |
+
"@babel/traverse@7.29.0": {
|
| 147 |
+
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 148 |
+
"dependencies": [
|
| 149 |
+
"@babel/code-frame",
|
| 150 |
+
"@babel/generator",
|
| 151 |
+
"@babel/helper-globals",
|
| 152 |
+
"@babel/parser",
|
| 153 |
+
"@babel/template",
|
| 154 |
+
"@babel/types",
|
| 155 |
+
"debug"
|
| 156 |
+
]
|
| 157 |
+
},
|
| 158 |
+
"@babel/types@7.29.0": {
|
| 159 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 160 |
+
"dependencies": [
|
| 161 |
+
"@babel/helper-string-parser",
|
| 162 |
+
"@babel/helper-validator-identifier"
|
| 163 |
+
]
|
| 164 |
+
},
|
| 165 |
+
"@esbuild/aix-ppc64@0.25.12": {
|
| 166 |
+
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
| 167 |
+
"os": ["aix"],
|
| 168 |
+
"cpu": ["ppc64"]
|
| 169 |
+
},
|
| 170 |
+
"@esbuild/android-arm64@0.25.12": {
|
| 171 |
+
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
| 172 |
+
"os": ["android"],
|
| 173 |
+
"cpu": ["arm64"]
|
| 174 |
+
},
|
| 175 |
+
"@esbuild/android-arm@0.25.12": {
|
| 176 |
+
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
| 177 |
+
"os": ["android"],
|
| 178 |
+
"cpu": ["arm"]
|
| 179 |
+
},
|
| 180 |
+
"@esbuild/android-x64@0.25.12": {
|
| 181 |
+
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
| 182 |
+
"os": ["android"],
|
| 183 |
+
"cpu": ["x64"]
|
| 184 |
+
},
|
| 185 |
+
"@esbuild/darwin-arm64@0.25.12": {
|
| 186 |
+
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
| 187 |
+
"os": ["darwin"],
|
| 188 |
+
"cpu": ["arm64"]
|
| 189 |
+
},
|
| 190 |
+
"@esbuild/darwin-x64@0.25.12": {
|
| 191 |
+
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
| 192 |
+
"os": ["darwin"],
|
| 193 |
+
"cpu": ["x64"]
|
| 194 |
+
},
|
| 195 |
+
"@esbuild/freebsd-arm64@0.25.12": {
|
| 196 |
+
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
| 197 |
+
"os": ["freebsd"],
|
| 198 |
+
"cpu": ["arm64"]
|
| 199 |
+
},
|
| 200 |
+
"@esbuild/freebsd-x64@0.25.12": {
|
| 201 |
+
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
| 202 |
+
"os": ["freebsd"],
|
| 203 |
+
"cpu": ["x64"]
|
| 204 |
+
},
|
| 205 |
+
"@esbuild/linux-arm64@0.25.12": {
|
| 206 |
+
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
| 207 |
+
"os": ["linux"],
|
| 208 |
+
"cpu": ["arm64"]
|
| 209 |
+
},
|
| 210 |
+
"@esbuild/linux-arm@0.25.12": {
|
| 211 |
+
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
| 212 |
+
"os": ["linux"],
|
| 213 |
+
"cpu": ["arm"]
|
| 214 |
+
},
|
| 215 |
+
"@esbuild/linux-ia32@0.25.12": {
|
| 216 |
+
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
| 217 |
+
"os": ["linux"],
|
| 218 |
+
"cpu": ["ia32"]
|
| 219 |
+
},
|
| 220 |
+
"@esbuild/linux-loong64@0.25.12": {
|
| 221 |
+
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
| 222 |
+
"os": ["linux"],
|
| 223 |
+
"cpu": ["loong64"]
|
| 224 |
+
},
|
| 225 |
+
"@esbuild/linux-mips64el@0.25.12": {
|
| 226 |
+
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
| 227 |
+
"os": ["linux"],
|
| 228 |
+
"cpu": ["mips64el"]
|
| 229 |
+
},
|
| 230 |
+
"@esbuild/linux-ppc64@0.25.12": {
|
| 231 |
+
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
| 232 |
+
"os": ["linux"],
|
| 233 |
+
"cpu": ["ppc64"]
|
| 234 |
+
},
|
| 235 |
+
"@esbuild/linux-riscv64@0.25.12": {
|
| 236 |
+
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
| 237 |
+
"os": ["linux"],
|
| 238 |
+
"cpu": ["riscv64"]
|
| 239 |
+
},
|
| 240 |
+
"@esbuild/linux-s390x@0.25.12": {
|
| 241 |
+
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
| 242 |
+
"os": ["linux"],
|
| 243 |
+
"cpu": ["s390x"]
|
| 244 |
+
},
|
| 245 |
+
"@esbuild/linux-x64@0.25.12": {
|
| 246 |
+
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
| 247 |
+
"os": ["linux"],
|
| 248 |
+
"cpu": ["x64"]
|
| 249 |
+
},
|
| 250 |
+
"@esbuild/netbsd-arm64@0.25.12": {
|
| 251 |
+
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
| 252 |
+
"os": ["netbsd"],
|
| 253 |
+
"cpu": ["arm64"]
|
| 254 |
+
},
|
| 255 |
+
"@esbuild/netbsd-x64@0.25.12": {
|
| 256 |
+
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
| 257 |
+
"os": ["netbsd"],
|
| 258 |
+
"cpu": ["x64"]
|
| 259 |
+
},
|
| 260 |
+
"@esbuild/openbsd-arm64@0.25.12": {
|
| 261 |
+
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
| 262 |
+
"os": ["openbsd"],
|
| 263 |
+
"cpu": ["arm64"]
|
| 264 |
+
},
|
| 265 |
+
"@esbuild/openbsd-x64@0.25.12": {
|
| 266 |
+
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
| 267 |
+
"os": ["openbsd"],
|
| 268 |
+
"cpu": ["x64"]
|
| 269 |
+
},
|
| 270 |
+
"@esbuild/openharmony-arm64@0.25.12": {
|
| 271 |
+
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
| 272 |
+
"os": ["openharmony"],
|
| 273 |
+
"cpu": ["arm64"]
|
| 274 |
+
},
|
| 275 |
+
"@esbuild/sunos-x64@0.25.12": {
|
| 276 |
+
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
| 277 |
+
"os": ["sunos"],
|
| 278 |
+
"cpu": ["x64"]
|
| 279 |
+
},
|
| 280 |
+
"@esbuild/win32-arm64@0.25.12": {
|
| 281 |
+
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
| 282 |
+
"os": ["win32"],
|
| 283 |
+
"cpu": ["arm64"]
|
| 284 |
+
},
|
| 285 |
+
"@esbuild/win32-ia32@0.25.12": {
|
| 286 |
+
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
| 287 |
+
"os": ["win32"],
|
| 288 |
+
"cpu": ["ia32"]
|
| 289 |
+
},
|
| 290 |
+
"@esbuild/win32-x64@0.25.12": {
|
| 291 |
+
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
| 292 |
+
"os": ["win32"],
|
| 293 |
+
"cpu": ["x64"]
|
| 294 |
+
},
|
| 295 |
+
"@jridgewell/gen-mapping@0.3.13": {
|
| 296 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 297 |
+
"dependencies": [
|
| 298 |
+
"@jridgewell/sourcemap-codec",
|
| 299 |
+
"@jridgewell/trace-mapping"
|
| 300 |
+
]
|
| 301 |
+
},
|
| 302 |
+
"@jridgewell/remapping@2.3.5": {
|
| 303 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 304 |
+
"dependencies": [
|
| 305 |
+
"@jridgewell/gen-mapping",
|
| 306 |
+
"@jridgewell/trace-mapping"
|
| 307 |
+
]
|
| 308 |
+
},
|
| 309 |
+
"@jridgewell/resolve-uri@3.1.2": {
|
| 310 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
|
| 311 |
+
},
|
| 312 |
+
"@jridgewell/sourcemap-codec@1.5.5": {
|
| 313 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
|
| 314 |
+
},
|
| 315 |
+
"@jridgewell/trace-mapping@0.3.31": {
|
| 316 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 317 |
+
"dependencies": [
|
| 318 |
+
"@jridgewell/resolve-uri",
|
| 319 |
+
"@jridgewell/sourcemap-codec"
|
| 320 |
+
]
|
| 321 |
+
},
|
| 322 |
+
"@rolldown/pluginutils@1.0.0-beta.27": {
|
| 323 |
+
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="
|
| 324 |
+
},
|
| 325 |
+
"@rollup/rollup-android-arm-eabi@4.60.3": {
|
| 326 |
+
"integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
|
| 327 |
+
"os": ["android"],
|
| 328 |
+
"cpu": ["arm"]
|
| 329 |
+
},
|
| 330 |
+
"@rollup/rollup-android-arm64@4.60.3": {
|
| 331 |
+
"integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
|
| 332 |
+
"os": ["android"],
|
| 333 |
+
"cpu": ["arm64"]
|
| 334 |
+
},
|
| 335 |
+
"@rollup/rollup-darwin-arm64@4.60.3": {
|
| 336 |
+
"integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
|
| 337 |
+
"os": ["darwin"],
|
| 338 |
+
"cpu": ["arm64"]
|
| 339 |
+
},
|
| 340 |
+
"@rollup/rollup-darwin-x64@4.60.3": {
|
| 341 |
+
"integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
|
| 342 |
+
"os": ["darwin"],
|
| 343 |
+
"cpu": ["x64"]
|
| 344 |
+
},
|
| 345 |
+
"@rollup/rollup-freebsd-arm64@4.60.3": {
|
| 346 |
+
"integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
|
| 347 |
+
"os": ["freebsd"],
|
| 348 |
+
"cpu": ["arm64"]
|
| 349 |
+
},
|
| 350 |
+
"@rollup/rollup-freebsd-x64@4.60.3": {
|
| 351 |
+
"integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
|
| 352 |
+
"os": ["freebsd"],
|
| 353 |
+
"cpu": ["x64"]
|
| 354 |
+
},
|
| 355 |
+
"@rollup/rollup-linux-arm-gnueabihf@4.60.3": {
|
| 356 |
+
"integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
|
| 357 |
+
"os": ["linux"],
|
| 358 |
+
"cpu": ["arm"]
|
| 359 |
+
},
|
| 360 |
+
"@rollup/rollup-linux-arm-musleabihf@4.60.3": {
|
| 361 |
+
"integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
|
| 362 |
+
"os": ["linux"],
|
| 363 |
+
"cpu": ["arm"]
|
| 364 |
+
},
|
| 365 |
+
"@rollup/rollup-linux-arm64-gnu@4.60.3": {
|
| 366 |
+
"integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
|
| 367 |
+
"os": ["linux"],
|
| 368 |
+
"cpu": ["arm64"]
|
| 369 |
+
},
|
| 370 |
+
"@rollup/rollup-linux-arm64-musl@4.60.3": {
|
| 371 |
+
"integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
|
| 372 |
+
"os": ["linux"],
|
| 373 |
+
"cpu": ["arm64"]
|
| 374 |
+
},
|
| 375 |
+
"@rollup/rollup-linux-loong64-gnu@4.60.3": {
|
| 376 |
+
"integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
|
| 377 |
+
"os": ["linux"],
|
| 378 |
+
"cpu": ["loong64"]
|
| 379 |
+
},
|
| 380 |
+
"@rollup/rollup-linux-loong64-musl@4.60.3": {
|
| 381 |
+
"integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
|
| 382 |
+
"os": ["linux"],
|
| 383 |
+
"cpu": ["loong64"]
|
| 384 |
+
},
|
| 385 |
+
"@rollup/rollup-linux-ppc64-gnu@4.60.3": {
|
| 386 |
+
"integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
|
| 387 |
+
"os": ["linux"],
|
| 388 |
+
"cpu": ["ppc64"]
|
| 389 |
+
},
|
| 390 |
+
"@rollup/rollup-linux-ppc64-musl@4.60.3": {
|
| 391 |
+
"integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
|
| 392 |
+
"os": ["linux"],
|
| 393 |
+
"cpu": ["ppc64"]
|
| 394 |
+
},
|
| 395 |
+
"@rollup/rollup-linux-riscv64-gnu@4.60.3": {
|
| 396 |
+
"integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
|
| 397 |
+
"os": ["linux"],
|
| 398 |
+
"cpu": ["riscv64"]
|
| 399 |
+
},
|
| 400 |
+
"@rollup/rollup-linux-riscv64-musl@4.60.3": {
|
| 401 |
+
"integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
|
| 402 |
+
"os": ["linux"],
|
| 403 |
+
"cpu": ["riscv64"]
|
| 404 |
+
},
|
| 405 |
+
"@rollup/rollup-linux-s390x-gnu@4.60.3": {
|
| 406 |
+
"integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
|
| 407 |
+
"os": ["linux"],
|
| 408 |
+
"cpu": ["s390x"]
|
| 409 |
+
},
|
| 410 |
+
"@rollup/rollup-linux-x64-gnu@4.60.3": {
|
| 411 |
+
"integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
|
| 412 |
+
"os": ["linux"],
|
| 413 |
+
"cpu": ["x64"]
|
| 414 |
+
},
|
| 415 |
+
"@rollup/rollup-linux-x64-musl@4.60.3": {
|
| 416 |
+
"integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
|
| 417 |
+
"os": ["linux"],
|
| 418 |
+
"cpu": ["x64"]
|
| 419 |
+
},
|
| 420 |
+
"@rollup/rollup-openbsd-x64@4.60.3": {
|
| 421 |
+
"integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
|
| 422 |
+
"os": ["openbsd"],
|
| 423 |
+
"cpu": ["x64"]
|
| 424 |
+
},
|
| 425 |
+
"@rollup/rollup-openharmony-arm64@4.60.3": {
|
| 426 |
+
"integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
|
| 427 |
+
"os": ["openharmony"],
|
| 428 |
+
"cpu": ["arm64"]
|
| 429 |
+
},
|
| 430 |
+
"@rollup/rollup-win32-arm64-msvc@4.60.3": {
|
| 431 |
+
"integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
|
| 432 |
+
"os": ["win32"],
|
| 433 |
+
"cpu": ["arm64"]
|
| 434 |
+
},
|
| 435 |
+
"@rollup/rollup-win32-ia32-msvc@4.60.3": {
|
| 436 |
+
"integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
|
| 437 |
+
"os": ["win32"],
|
| 438 |
+
"cpu": ["ia32"]
|
| 439 |
+
},
|
| 440 |
+
"@rollup/rollup-win32-x64-gnu@4.60.3": {
|
| 441 |
+
"integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
|
| 442 |
+
"os": ["win32"],
|
| 443 |
+
"cpu": ["x64"]
|
| 444 |
+
},
|
| 445 |
+
"@rollup/rollup-win32-x64-msvc@4.60.3": {
|
| 446 |
+
"integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
|
| 447 |
+
"os": ["win32"],
|
| 448 |
+
"cpu": ["x64"]
|
| 449 |
+
},
|
| 450 |
+
"@tailwindcss/node@4.3.0": {
|
| 451 |
+
"integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
|
| 452 |
+
"dependencies": [
|
| 453 |
+
"@jridgewell/remapping",
|
| 454 |
+
"enhanced-resolve",
|
| 455 |
+
"jiti",
|
| 456 |
+
"lightningcss",
|
| 457 |
+
"magic-string",
|
| 458 |
+
"source-map-js",
|
| 459 |
+
"tailwindcss"
|
| 460 |
+
]
|
| 461 |
+
},
|
| 462 |
+
"@tailwindcss/oxide-android-arm64@4.3.0": {
|
| 463 |
+
"integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
|
| 464 |
+
"os": ["android"],
|
| 465 |
+
"cpu": ["arm64"]
|
| 466 |
+
},
|
| 467 |
+
"@tailwindcss/oxide-darwin-arm64@4.3.0": {
|
| 468 |
+
"integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
|
| 469 |
+
"os": ["darwin"],
|
| 470 |
+
"cpu": ["arm64"]
|
| 471 |
+
},
|
| 472 |
+
"@tailwindcss/oxide-darwin-x64@4.3.0": {
|
| 473 |
+
"integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
|
| 474 |
+
"os": ["darwin"],
|
| 475 |
+
"cpu": ["x64"]
|
| 476 |
+
},
|
| 477 |
+
"@tailwindcss/oxide-freebsd-x64@4.3.0": {
|
| 478 |
+
"integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
|
| 479 |
+
"os": ["freebsd"],
|
| 480 |
+
"cpu": ["x64"]
|
| 481 |
+
},
|
| 482 |
+
"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0": {
|
| 483 |
+
"integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
|
| 484 |
+
"os": ["linux"],
|
| 485 |
+
"cpu": ["arm"]
|
| 486 |
+
},
|
| 487 |
+
"@tailwindcss/oxide-linux-arm64-gnu@4.3.0": {
|
| 488 |
+
"integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
|
| 489 |
+
"os": ["linux"],
|
| 490 |
+
"cpu": ["arm64"]
|
| 491 |
+
},
|
| 492 |
+
"@tailwindcss/oxide-linux-arm64-musl@4.3.0": {
|
| 493 |
+
"integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
|
| 494 |
+
"os": ["linux"],
|
| 495 |
+
"cpu": ["arm64"]
|
| 496 |
+
},
|
| 497 |
+
"@tailwindcss/oxide-linux-x64-gnu@4.3.0": {
|
| 498 |
+
"integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
|
| 499 |
+
"os": ["linux"],
|
| 500 |
+
"cpu": ["x64"]
|
| 501 |
+
},
|
| 502 |
+
"@tailwindcss/oxide-linux-x64-musl@4.3.0": {
|
| 503 |
+
"integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
|
| 504 |
+
"os": ["linux"],
|
| 505 |
+
"cpu": ["x64"]
|
| 506 |
+
},
|
| 507 |
+
"@tailwindcss/oxide-wasm32-wasi@4.3.0": {
|
| 508 |
+
"integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
|
| 509 |
+
"cpu": ["wasm32"]
|
| 510 |
+
},
|
| 511 |
+
"@tailwindcss/oxide-win32-arm64-msvc@4.3.0": {
|
| 512 |
+
"integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
|
| 513 |
+
"os": ["win32"],
|
| 514 |
+
"cpu": ["arm64"]
|
| 515 |
+
},
|
| 516 |
+
"@tailwindcss/oxide-win32-x64-msvc@4.3.0": {
|
| 517 |
+
"integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
|
| 518 |
+
"os": ["win32"],
|
| 519 |
+
"cpu": ["x64"]
|
| 520 |
+
},
|
| 521 |
+
"@tailwindcss/oxide@4.3.0": {
|
| 522 |
+
"integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
|
| 523 |
+
"optionalDependencies": [
|
| 524 |
+
"@tailwindcss/oxide-android-arm64",
|
| 525 |
+
"@tailwindcss/oxide-darwin-arm64",
|
| 526 |
+
"@tailwindcss/oxide-darwin-x64",
|
| 527 |
+
"@tailwindcss/oxide-freebsd-x64",
|
| 528 |
+
"@tailwindcss/oxide-linux-arm-gnueabihf",
|
| 529 |
+
"@tailwindcss/oxide-linux-arm64-gnu",
|
| 530 |
+
"@tailwindcss/oxide-linux-arm64-musl",
|
| 531 |
+
"@tailwindcss/oxide-linux-x64-gnu",
|
| 532 |
+
"@tailwindcss/oxide-linux-x64-musl",
|
| 533 |
+
"@tailwindcss/oxide-wasm32-wasi",
|
| 534 |
+
"@tailwindcss/oxide-win32-arm64-msvc",
|
| 535 |
+
"@tailwindcss/oxide-win32-x64-msvc"
|
| 536 |
+
]
|
| 537 |
+
},
|
| 538 |
+
"@tailwindcss/vite@4.3.0_vite@6.4.2__picomatch@4.0.4": {
|
| 539 |
+
"integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==",
|
| 540 |
+
"dependencies": [
|
| 541 |
+
"@tailwindcss/node",
|
| 542 |
+
"@tailwindcss/oxide",
|
| 543 |
+
"tailwindcss",
|
| 544 |
+
"vite"
|
| 545 |
+
]
|
| 546 |
+
},
|
| 547 |
+
"@tauri-apps/api@2.11.0": {
|
| 548 |
+
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="
|
| 549 |
+
},
|
| 550 |
+
"@tauri-apps/cli-darwin-arm64@2.11.1": {
|
| 551 |
+
"integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==",
|
| 552 |
+
"os": ["darwin"],
|
| 553 |
+
"cpu": ["arm64"]
|
| 554 |
+
},
|
| 555 |
+
"@tauri-apps/cli-darwin-x64@2.11.1": {
|
| 556 |
+
"integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==",
|
| 557 |
+
"os": ["darwin"],
|
| 558 |
+
"cpu": ["x64"]
|
| 559 |
+
},
|
| 560 |
+
"@tauri-apps/cli-linux-arm-gnueabihf@2.11.1": {
|
| 561 |
+
"integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==",
|
| 562 |
+
"os": ["linux"],
|
| 563 |
+
"cpu": ["arm"]
|
| 564 |
+
},
|
| 565 |
+
"@tauri-apps/cli-linux-arm64-gnu@2.11.1": {
|
| 566 |
+
"integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==",
|
| 567 |
+
"os": ["linux"],
|
| 568 |
+
"cpu": ["arm64"]
|
| 569 |
+
},
|
| 570 |
+
"@tauri-apps/cli-linux-arm64-musl@2.11.1": {
|
| 571 |
+
"integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==",
|
| 572 |
+
"os": ["linux"],
|
| 573 |
+
"cpu": ["arm64"]
|
| 574 |
+
},
|
| 575 |
+
"@tauri-apps/cli-linux-riscv64-gnu@2.11.1": {
|
| 576 |
+
"integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==",
|
| 577 |
+
"os": ["linux"],
|
| 578 |
+
"cpu": ["riscv64"]
|
| 579 |
+
},
|
| 580 |
+
"@tauri-apps/cli-linux-x64-gnu@2.11.1": {
|
| 581 |
+
"integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==",
|
| 582 |
+
"os": ["linux"],
|
| 583 |
+
"cpu": ["x64"]
|
| 584 |
+
},
|
| 585 |
+
"@tauri-apps/cli-linux-x64-musl@2.11.1": {
|
| 586 |
+
"integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==",
|
| 587 |
+
"os": ["linux"],
|
| 588 |
+
"cpu": ["x64"]
|
| 589 |
+
},
|
| 590 |
+
"@tauri-apps/cli-win32-arm64-msvc@2.11.1": {
|
| 591 |
+
"integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==",
|
| 592 |
+
"os": ["win32"],
|
| 593 |
+
"cpu": ["arm64"]
|
| 594 |
+
},
|
| 595 |
+
"@tauri-apps/cli-win32-ia32-msvc@2.11.1": {
|
| 596 |
+
"integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==",
|
| 597 |
+
"os": ["win32"],
|
| 598 |
+
"cpu": ["ia32"]
|
| 599 |
+
},
|
| 600 |
+
"@tauri-apps/cli-win32-x64-msvc@2.11.1": {
|
| 601 |
+
"integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==",
|
| 602 |
+
"os": ["win32"],
|
| 603 |
+
"cpu": ["x64"]
|
| 604 |
+
},
|
| 605 |
+
"@tauri-apps/cli@2.11.1": {
|
| 606 |
+
"integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==",
|
| 607 |
+
"optionalDependencies": [
|
| 608 |
+
"@tauri-apps/cli-darwin-arm64",
|
| 609 |
+
"@tauri-apps/cli-darwin-x64",
|
| 610 |
+
"@tauri-apps/cli-linux-arm-gnueabihf",
|
| 611 |
+
"@tauri-apps/cli-linux-arm64-gnu",
|
| 612 |
+
"@tauri-apps/cli-linux-arm64-musl",
|
| 613 |
+
"@tauri-apps/cli-linux-riscv64-gnu",
|
| 614 |
+
"@tauri-apps/cli-linux-x64-gnu",
|
| 615 |
+
"@tauri-apps/cli-linux-x64-musl",
|
| 616 |
+
"@tauri-apps/cli-win32-arm64-msvc",
|
| 617 |
+
"@tauri-apps/cli-win32-ia32-msvc",
|
| 618 |
+
"@tauri-apps/cli-win32-x64-msvc"
|
| 619 |
+
],
|
| 620 |
+
"bin": true
|
| 621 |
+
},
|
| 622 |
+
"@tauri-apps/plugin-clipboard-manager@2.3.2": {
|
| 623 |
+
"integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==",
|
| 624 |
+
"dependencies": [
|
| 625 |
+
"@tauri-apps/api"
|
| 626 |
+
]
|
| 627 |
+
},
|
| 628 |
+
"@tauri-apps/plugin-fs@2.5.1": {
|
| 629 |
+
"integrity": "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==",
|
| 630 |
+
"dependencies": [
|
| 631 |
+
"@tauri-apps/api"
|
| 632 |
+
]
|
| 633 |
+
},
|
| 634 |
+
"@tauri-apps/plugin-opener@2.5.3": {
|
| 635 |
+
"integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==",
|
| 636 |
+
"dependencies": [
|
| 637 |
+
"@tauri-apps/api"
|
| 638 |
+
]
|
| 639 |
+
},
|
| 640 |
+
"@tauri-apps/plugin-store@2.4.3": {
|
| 641 |
+
"integrity": "sha512-9LWPj9yMphRi9czEtUv87XHbl1b6xgd9EXpPrUnq6nG7+nbtoF84d4Kwz9xhAv/Hf30sr58pq7EOlyI936y8qw==",
|
| 642 |
+
"dependencies": [
|
| 643 |
+
"@tauri-apps/api"
|
| 644 |
+
]
|
| 645 |
+
},
|
| 646 |
+
"@tauri-apps/plugin-stronghold@2.3.1": {
|
| 647 |
+
"integrity": "sha512-zFbD1Apk/VFdWaoGaoKcouRrZnzLFiNY9b1KDeBaN47sMaMHRYIa+ZDhvbzMOyH314+OHCQBXfe8I/ph59Lp9g==",
|
| 648 |
+
"dependencies": [
|
| 649 |
+
"@tauri-apps/api"
|
| 650 |
+
]
|
| 651 |
+
},
|
| 652 |
+
"@types/babel__core@7.20.5": {
|
| 653 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 654 |
+
"dependencies": [
|
| 655 |
+
"@babel/parser",
|
| 656 |
+
"@babel/types",
|
| 657 |
+
"@types/babel__generator",
|
| 658 |
+
"@types/babel__template",
|
| 659 |
+
"@types/babel__traverse"
|
| 660 |
+
]
|
| 661 |
+
},
|
| 662 |
+
"@types/babel__generator@7.27.0": {
|
| 663 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 664 |
+
"dependencies": [
|
| 665 |
+
"@babel/types"
|
| 666 |
+
]
|
| 667 |
+
},
|
| 668 |
+
"@types/babel__template@7.4.4": {
|
| 669 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 670 |
+
"dependencies": [
|
| 671 |
+
"@babel/parser",
|
| 672 |
+
"@babel/types"
|
| 673 |
+
]
|
| 674 |
+
},
|
| 675 |
+
"@types/babel__traverse@7.28.0": {
|
| 676 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 677 |
+
"dependencies": [
|
| 678 |
+
"@babel/types"
|
| 679 |
+
]
|
| 680 |
+
},
|
| 681 |
+
"@types/estree@1.0.8": {
|
| 682 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
|
| 683 |
+
},
|
| 684 |
+
"@types/react-dom@19.2.3_@types+react@19.2.14": {
|
| 685 |
+
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
| 686 |
+
"dependencies": [
|
| 687 |
+
"@types/react"
|
| 688 |
+
]
|
| 689 |
+
},
|
| 690 |
+
"@types/react@19.2.14": {
|
| 691 |
+
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
| 692 |
+
"dependencies": [
|
| 693 |
+
"csstype"
|
| 694 |
+
]
|
| 695 |
+
},
|
| 696 |
+
"@vitejs/plugin-react@4.7.0_vite@6.4.2__picomatch@4.0.4_@babel+core@7.29.0": {
|
| 697 |
+
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
| 698 |
+
"dependencies": [
|
| 699 |
+
"@babel/core",
|
| 700 |
+
"@babel/plugin-transform-react-jsx-self",
|
| 701 |
+
"@babel/plugin-transform-react-jsx-source",
|
| 702 |
+
"@rolldown/pluginutils",
|
| 703 |
+
"@types/babel__core",
|
| 704 |
+
"react-refresh",
|
| 705 |
+
"vite"
|
| 706 |
+
]
|
| 707 |
+
},
|
| 708 |
+
"autoprefixer@10.5.0_postcss@8.5.14": {
|
| 709 |
+
"integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
|
| 710 |
+
"dependencies": [
|
| 711 |
+
"browserslist",
|
| 712 |
+
"caniuse-lite",
|
| 713 |
+
"fraction.js",
|
| 714 |
+
"picocolors",
|
| 715 |
+
"postcss",
|
| 716 |
+
"postcss-value-parser"
|
| 717 |
+
],
|
| 718 |
+
"bin": true
|
| 719 |
+
},
|
| 720 |
+
"baseline-browser-mapping@2.10.29": {
|
| 721 |
+
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
|
| 722 |
+
"bin": true
|
| 723 |
+
},
|
| 724 |
+
"browserslist@4.28.2": {
|
| 725 |
+
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
| 726 |
+
"dependencies": [
|
| 727 |
+
"baseline-browser-mapping",
|
| 728 |
+
"caniuse-lite",
|
| 729 |
+
"electron-to-chromium",
|
| 730 |
+
"node-releases",
|
| 731 |
+
"update-browserslist-db"
|
| 732 |
+
],
|
| 733 |
+
"bin": true
|
| 734 |
+
},
|
| 735 |
+
"caniuse-lite@1.0.30001792": {
|
| 736 |
+
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="
|
| 737 |
+
},
|
| 738 |
+
"convert-source-map@2.0.0": {
|
| 739 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
| 740 |
+
},
|
| 741 |
+
"csstype@3.2.3": {
|
| 742 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
| 743 |
+
},
|
| 744 |
+
"debug@4.4.3": {
|
| 745 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 746 |
+
"dependencies": [
|
| 747 |
+
"ms"
|
| 748 |
+
]
|
| 749 |
+
},
|
| 750 |
+
"detect-libc@2.1.2": {
|
| 751 |
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="
|
| 752 |
+
},
|
| 753 |
+
"electron-to-chromium@1.5.353": {
|
| 754 |
+
"integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="
|
| 755 |
+
},
|
| 756 |
+
"enhanced-resolve@5.21.2": {
|
| 757 |
+
"integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==",
|
| 758 |
+
"dependencies": [
|
| 759 |
+
"graceful-fs",
|
| 760 |
+
"tapable"
|
| 761 |
+
]
|
| 762 |
+
},
|
| 763 |
+
"esbuild@0.25.12": {
|
| 764 |
+
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
| 765 |
+
"optionalDependencies": [
|
| 766 |
+
"@esbuild/aix-ppc64",
|
| 767 |
+
"@esbuild/android-arm",
|
| 768 |
+
"@esbuild/android-arm64",
|
| 769 |
+
"@esbuild/android-x64",
|
| 770 |
+
"@esbuild/darwin-arm64",
|
| 771 |
+
"@esbuild/darwin-x64",
|
| 772 |
+
"@esbuild/freebsd-arm64",
|
| 773 |
+
"@esbuild/freebsd-x64",
|
| 774 |
+
"@esbuild/linux-arm",
|
| 775 |
+
"@esbuild/linux-arm64",
|
| 776 |
+
"@esbuild/linux-ia32",
|
| 777 |
+
"@esbuild/linux-loong64",
|
| 778 |
+
"@esbuild/linux-mips64el",
|
| 779 |
+
"@esbuild/linux-ppc64",
|
| 780 |
+
"@esbuild/linux-riscv64",
|
| 781 |
+
"@esbuild/linux-s390x",
|
| 782 |
+
"@esbuild/linux-x64",
|
| 783 |
+
"@esbuild/netbsd-arm64",
|
| 784 |
+
"@esbuild/netbsd-x64",
|
| 785 |
+
"@esbuild/openbsd-arm64",
|
| 786 |
+
"@esbuild/openbsd-x64",
|
| 787 |
+
"@esbuild/openharmony-arm64",
|
| 788 |
+
"@esbuild/sunos-x64",
|
| 789 |
+
"@esbuild/win32-arm64",
|
| 790 |
+
"@esbuild/win32-ia32",
|
| 791 |
+
"@esbuild/win32-x64"
|
| 792 |
+
],
|
| 793 |
+
"scripts": true,
|
| 794 |
+
"bin": true
|
| 795 |
+
},
|
| 796 |
+
"escalade@3.2.0": {
|
| 797 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="
|
| 798 |
+
},
|
| 799 |
+
"fdir@6.5.0_picomatch@4.0.4": {
|
| 800 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 801 |
+
"dependencies": [
|
| 802 |
+
"picomatch"
|
| 803 |
+
],
|
| 804 |
+
"optionalPeers": [
|
| 805 |
+
"picomatch"
|
| 806 |
+
]
|
| 807 |
+
},
|
| 808 |
+
"fraction.js@5.3.4": {
|
| 809 |
+
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="
|
| 810 |
+
},
|
| 811 |
+
"framer-motion@12.38.0_react@19.2.6_react-dom@19.2.6__react@19.2.6": {
|
| 812 |
+
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
| 813 |
+
"dependencies": [
|
| 814 |
+
"motion-dom",
|
| 815 |
+
"motion-utils",
|
| 816 |
+
"react",
|
| 817 |
+
"react-dom",
|
| 818 |
+
"tslib"
|
| 819 |
+
],
|
| 820 |
+
"optionalPeers": [
|
| 821 |
+
"react",
|
| 822 |
+
"react-dom"
|
| 823 |
+
]
|
| 824 |
+
},
|
| 825 |
+
"fsevents@2.3.3": {
|
| 826 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 827 |
+
"os": ["darwin"],
|
| 828 |
+
"scripts": true
|
| 829 |
+
},
|
| 830 |
+
"gensync@1.0.0-beta.2": {
|
| 831 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
|
| 832 |
+
},
|
| 833 |
+
"graceful-fs@4.2.11": {
|
| 834 |
+
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
| 835 |
+
},
|
| 836 |
+
"jiti@2.7.0": {
|
| 837 |
+
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
| 838 |
+
"bin": true
|
| 839 |
+
},
|
| 840 |
+
"js-tokens@4.0.0": {
|
| 841 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
| 842 |
+
},
|
| 843 |
+
"jsesc@3.1.0": {
|
| 844 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 845 |
+
"bin": true
|
| 846 |
+
},
|
| 847 |
+
"json5@2.2.3": {
|
| 848 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 849 |
+
"bin": true
|
| 850 |
+
},
|
| 851 |
+
"lightningcss-android-arm64@1.32.0": {
|
| 852 |
+
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
| 853 |
+
"os": ["android"],
|
| 854 |
+
"cpu": ["arm64"]
|
| 855 |
+
},
|
| 856 |
+
"lightningcss-darwin-arm64@1.32.0": {
|
| 857 |
+
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
| 858 |
+
"os": ["darwin"],
|
| 859 |
+
"cpu": ["arm64"]
|
| 860 |
+
},
|
| 861 |
+
"lightningcss-darwin-x64@1.32.0": {
|
| 862 |
+
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
| 863 |
+
"os": ["darwin"],
|
| 864 |
+
"cpu": ["x64"]
|
| 865 |
+
},
|
| 866 |
+
"lightningcss-freebsd-x64@1.32.0": {
|
| 867 |
+
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
| 868 |
+
"os": ["freebsd"],
|
| 869 |
+
"cpu": ["x64"]
|
| 870 |
+
},
|
| 871 |
+
"lightningcss-linux-arm-gnueabihf@1.32.0": {
|
| 872 |
+
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
| 873 |
+
"os": ["linux"],
|
| 874 |
+
"cpu": ["arm"]
|
| 875 |
+
},
|
| 876 |
+
"lightningcss-linux-arm64-gnu@1.32.0": {
|
| 877 |
+
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
| 878 |
+
"os": ["linux"],
|
| 879 |
+
"cpu": ["arm64"]
|
| 880 |
+
},
|
| 881 |
+
"lightningcss-linux-arm64-musl@1.32.0": {
|
| 882 |
+
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
| 883 |
+
"os": ["linux"],
|
| 884 |
+
"cpu": ["arm64"]
|
| 885 |
+
},
|
| 886 |
+
"lightningcss-linux-x64-gnu@1.32.0": {
|
| 887 |
+
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
| 888 |
+
"os": ["linux"],
|
| 889 |
+
"cpu": ["x64"]
|
| 890 |
+
},
|
| 891 |
+
"lightningcss-linux-x64-musl@1.32.0": {
|
| 892 |
+
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
| 893 |
+
"os": ["linux"],
|
| 894 |
+
"cpu": ["x64"]
|
| 895 |
+
},
|
| 896 |
+
"lightningcss-win32-arm64-msvc@1.32.0": {
|
| 897 |
+
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
| 898 |
+
"os": ["win32"],
|
| 899 |
+
"cpu": ["arm64"]
|
| 900 |
+
},
|
| 901 |
+
"lightningcss-win32-x64-msvc@1.32.0": {
|
| 902 |
+
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
| 903 |
+
"os": ["win32"],
|
| 904 |
+
"cpu": ["x64"]
|
| 905 |
+
},
|
| 906 |
+
"lightningcss@1.32.0": {
|
| 907 |
+
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
| 908 |
+
"dependencies": [
|
| 909 |
+
"detect-libc"
|
| 910 |
+
],
|
| 911 |
+
"optionalDependencies": [
|
| 912 |
+
"lightningcss-android-arm64",
|
| 913 |
+
"lightningcss-darwin-arm64",
|
| 914 |
+
"lightningcss-darwin-x64",
|
| 915 |
+
"lightningcss-freebsd-x64",
|
| 916 |
+
"lightningcss-linux-arm-gnueabihf",
|
| 917 |
+
"lightningcss-linux-arm64-gnu",
|
| 918 |
+
"lightningcss-linux-arm64-musl",
|
| 919 |
+
"lightningcss-linux-x64-gnu",
|
| 920 |
+
"lightningcss-linux-x64-musl",
|
| 921 |
+
"lightningcss-win32-arm64-msvc",
|
| 922 |
+
"lightningcss-win32-x64-msvc"
|
| 923 |
+
]
|
| 924 |
+
},
|
| 925 |
+
"lru-cache@5.1.1": {
|
| 926 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 927 |
+
"dependencies": [
|
| 928 |
+
"yallist"
|
| 929 |
+
]
|
| 930 |
+
},
|
| 931 |
+
"lucide-react@0.468.0_react@19.2.6": {
|
| 932 |
+
"integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
|
| 933 |
+
"dependencies": [
|
| 934 |
+
"react"
|
| 935 |
+
]
|
| 936 |
+
},
|
| 937 |
+
"magic-string@0.30.21": {
|
| 938 |
+
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
| 939 |
+
"dependencies": [
|
| 940 |
+
"@jridgewell/sourcemap-codec"
|
| 941 |
+
]
|
| 942 |
+
},
|
| 943 |
+
"motion-dom@12.38.0": {
|
| 944 |
+
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
| 945 |
+
"dependencies": [
|
| 946 |
+
"motion-utils"
|
| 947 |
+
]
|
| 948 |
+
},
|
| 949 |
+
"motion-utils@12.36.0": {
|
| 950 |
+
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="
|
| 951 |
+
},
|
| 952 |
+
"motion@12.38.0_react@19.2.6_react-dom@19.2.6__react@19.2.6": {
|
| 953 |
+
"integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
|
| 954 |
+
"dependencies": [
|
| 955 |
+
"framer-motion",
|
| 956 |
+
"react",
|
| 957 |
+
"react-dom",
|
| 958 |
+
"tslib"
|
| 959 |
+
],
|
| 960 |
+
"optionalPeers": [
|
| 961 |
+
"react",
|
| 962 |
+
"react-dom"
|
| 963 |
+
]
|
| 964 |
+
},
|
| 965 |
+
"ms@2.1.3": {
|
| 966 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
| 967 |
+
},
|
| 968 |
+
"nanoid@3.3.12": {
|
| 969 |
+
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
| 970 |
+
"bin": true
|
| 971 |
+
},
|
| 972 |
+
"node-releases@2.0.38": {
|
| 973 |
+
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="
|
| 974 |
+
},
|
| 975 |
+
"picocolors@1.1.1": {
|
| 976 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
| 977 |
+
},
|
| 978 |
+
"picomatch@4.0.4": {
|
| 979 |
+
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
|
| 980 |
+
},
|
| 981 |
+
"postcss-value-parser@4.2.0": {
|
| 982 |
+
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
| 983 |
+
},
|
| 984 |
+
"postcss@8.5.14": {
|
| 985 |
+
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
| 986 |
+
"dependencies": [
|
| 987 |
+
"nanoid",
|
| 988 |
+
"picocolors",
|
| 989 |
+
"source-map-js"
|
| 990 |
+
]
|
| 991 |
+
},
|
| 992 |
+
"react-dom@19.2.6_react@19.2.6": {
|
| 993 |
+
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
|
| 994 |
+
"dependencies": [
|
| 995 |
+
"react",
|
| 996 |
+
"scheduler"
|
| 997 |
+
]
|
| 998 |
+
},
|
| 999 |
+
"react-refresh@0.17.0": {
|
| 1000 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="
|
| 1001 |
+
},
|
| 1002 |
+
"react@19.2.6": {
|
| 1003 |
+
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="
|
| 1004 |
+
},
|
| 1005 |
+
"rollup@4.60.3": {
|
| 1006 |
+
"integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
|
| 1007 |
+
"dependencies": [
|
| 1008 |
+
"@types/estree"
|
| 1009 |
+
],
|
| 1010 |
+
"optionalDependencies": [
|
| 1011 |
+
"@rollup/rollup-android-arm-eabi",
|
| 1012 |
+
"@rollup/rollup-android-arm64",
|
| 1013 |
+
"@rollup/rollup-darwin-arm64",
|
| 1014 |
+
"@rollup/rollup-darwin-x64",
|
| 1015 |
+
"@rollup/rollup-freebsd-arm64",
|
| 1016 |
+
"@rollup/rollup-freebsd-x64",
|
| 1017 |
+
"@rollup/rollup-linux-arm-gnueabihf",
|
| 1018 |
+
"@rollup/rollup-linux-arm-musleabihf",
|
| 1019 |
+
"@rollup/rollup-linux-arm64-gnu",
|
| 1020 |
+
"@rollup/rollup-linux-arm64-musl",
|
| 1021 |
+
"@rollup/rollup-linux-loong64-gnu",
|
| 1022 |
+
"@rollup/rollup-linux-loong64-musl",
|
| 1023 |
+
"@rollup/rollup-linux-ppc64-gnu",
|
| 1024 |
+
"@rollup/rollup-linux-ppc64-musl",
|
| 1025 |
+
"@rollup/rollup-linux-riscv64-gnu",
|
| 1026 |
+
"@rollup/rollup-linux-riscv64-musl",
|
| 1027 |
+
"@rollup/rollup-linux-s390x-gnu",
|
| 1028 |
+
"@rollup/rollup-linux-x64-gnu",
|
| 1029 |
+
"@rollup/rollup-linux-x64-musl",
|
| 1030 |
+
"@rollup/rollup-openbsd-x64",
|
| 1031 |
+
"@rollup/rollup-openharmony-arm64",
|
| 1032 |
+
"@rollup/rollup-win32-arm64-msvc",
|
| 1033 |
+
"@rollup/rollup-win32-ia32-msvc",
|
| 1034 |
+
"@rollup/rollup-win32-x64-gnu",
|
| 1035 |
+
"@rollup/rollup-win32-x64-msvc",
|
| 1036 |
+
"fsevents"
|
| 1037 |
+
],
|
| 1038 |
+
"bin": true
|
| 1039 |
+
},
|
| 1040 |
+
"scheduler@0.27.0": {
|
| 1041 |
+
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
|
| 1042 |
+
},
|
| 1043 |
+
"semver@6.3.1": {
|
| 1044 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 1045 |
+
"bin": true
|
| 1046 |
+
},
|
| 1047 |
+
"source-map-js@1.2.1": {
|
| 1048 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
| 1049 |
+
},
|
| 1050 |
+
"tailwindcss@4.3.0": {
|
| 1051 |
+
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="
|
| 1052 |
+
},
|
| 1053 |
+
"tapable@2.3.3": {
|
| 1054 |
+
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="
|
| 1055 |
+
},
|
| 1056 |
+
"tinyglobby@0.2.16_picomatch@4.0.4": {
|
| 1057 |
+
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
| 1058 |
+
"dependencies": [
|
| 1059 |
+
"fdir",
|
| 1060 |
+
"picomatch"
|
| 1061 |
+
]
|
| 1062 |
+
},
|
| 1063 |
+
"tslib@2.8.1": {
|
| 1064 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
| 1065 |
+
},
|
| 1066 |
+
"typescript@5.6.3": {
|
| 1067 |
+
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
| 1068 |
+
"bin": true
|
| 1069 |
+
},
|
| 1070 |
+
"update-browserslist-db@1.2.3_browserslist@4.28.2": {
|
| 1071 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 1072 |
+
"dependencies": [
|
| 1073 |
+
"browserslist",
|
| 1074 |
+
"escalade",
|
| 1075 |
+
"picocolors"
|
| 1076 |
+
],
|
| 1077 |
+
"bin": true
|
| 1078 |
+
},
|
| 1079 |
+
"vite@6.4.2_picomatch@4.0.4": {
|
| 1080 |
+
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
| 1081 |
+
"dependencies": [
|
| 1082 |
+
"esbuild",
|
| 1083 |
+
"fdir",
|
| 1084 |
+
"picomatch",
|
| 1085 |
+
"postcss",
|
| 1086 |
+
"rollup",
|
| 1087 |
+
"tinyglobby"
|
| 1088 |
+
],
|
| 1089 |
+
"optionalDependencies": [
|
| 1090 |
+
"fsevents"
|
| 1091 |
+
],
|
| 1092 |
+
"bin": true
|
| 1093 |
+
},
|
| 1094 |
+
"yallist@3.1.1": {
|
| 1095 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
| 1096 |
+
}
|
| 1097 |
+
},
|
| 1098 |
+
"workspace": {
|
| 1099 |
+
"dependencies": [
|
| 1100 |
+
"npm:react-dom@19",
|
| 1101 |
+
"npm:react@19"
|
| 1102 |
+
],
|
| 1103 |
+
"packageJson": {
|
| 1104 |
+
"dependencies": [
|
| 1105 |
+
"npm:@tailwindcss/vite@4",
|
| 1106 |
+
"npm:@tauri-apps/api@2",
|
| 1107 |
+
"npm:@tauri-apps/cli@2",
|
| 1108 |
+
"npm:@tauri-apps/plugin-clipboard-manager@2",
|
| 1109 |
+
"npm:@tauri-apps/plugin-fs@2",
|
| 1110 |
+
"npm:@tauri-apps/plugin-opener@2",
|
| 1111 |
+
"npm:@tauri-apps/plugin-store@2",
|
| 1112 |
+
"npm:@tauri-apps/plugin-stronghold@2",
|
| 1113 |
+
"npm:@types/react-dom@19",
|
| 1114 |
+
"npm:@types/react@19",
|
| 1115 |
+
"npm:@vitejs/plugin-react@^4.3.0",
|
| 1116 |
+
"npm:autoprefixer@^10.4.20",
|
| 1117 |
+
"npm:lucide-react@0.468",
|
| 1118 |
+
"npm:motion@12",
|
| 1119 |
+
"npm:postcss@^8.4.49",
|
| 1120 |
+
"npm:react-dom@19",
|
| 1121 |
+
"npm:react@19",
|
| 1122 |
+
"npm:tailwindcss@4",
|
| 1123 |
+
"npm:typescript@~5.6.2",
|
| 1124 |
+
"npm:vite@6"
|
| 1125 |
+
]
|
| 1126 |
+
}
|
| 1127 |
+
}
|
| 1128 |
+
}
|
docs/ARCHITECTURE_PIVOT.md
CHANGED
|
@@ -1,43 +1,43 @@
|
|
| 1 |
-
# Browser-First Era Archive
|
| 2 |
-
|
| 3 |
-
This document marks the architectural pivot from **browser-first** (Muse Alpha v0.2) to **board-first** (RefStudio v1.0).
|
| 4 |
-
|
| 5 |
-
## What was the browser-first approach?
|
| 6 |
-
- Main workspace was a web browser with tabs
|
| 7 |
-
- Sidebar navigation between Home/Browser/Library/Board/Settings views
|
| 8 |
-
- Board was a secondary view accessed from the sidebar
|
| 9 |
-
- Image hover overlay injected into browser tabs for saving references
|
| 10 |
-
- Ad-blocking engine for browsing comfort
|
| 11 |
-
|
| 12 |
-
## Why the pivot?
|
| 13 |
-
The core use case is **reference organization**, not web browsing. Artists need:
|
| 14 |
-
- An infinite canvas as the PRIMARY workspace (like PureRef)
|
| 15 |
-
- Browser as a TOOL (embedded panel) to find references, not the main UI
|
| 16 |
-
- Zero-friction image capture from browser → canvas
|
| 17 |
-
- Always-on-top mode for overlaying references while working in art apps
|
| 18 |
-
- Click-through transparency for tracing
|
| 19 |
-
|
| 20 |
-
## What's preserved from browser-first era?
|
| 21 |
-
- Rust adblock engine (`adblock/` module)
|
| 22 |
-
- Tauri child webview architecture for embedded browser
|
| 23 |
-
- Image download + library persistence (`library.rs`, `persistence.rs`)
|
| 24 |
-
- Board item CRUD (`board.rs`)
|
| 25 |
-
- SQLite migrations
|
| 26 |
-
- Credentials/stronghold setup
|
| 27 |
-
|
| 28 |
-
## New architecture (board-first / RefStudio)
|
| 29 |
-
- Infinite canvas is the main and only workspace
|
| 30 |
-
- Browser panel slides in from the right as a capture tool
|
| 31 |
-
- Library panel slides in from the left for asset management
|
| 32 |
-
- Three.js GPU-accelerated image rendering with shaders
|
| 33 |
-
- Annotations (freehand drawing) directly on canvas
|
| 34 |
-
- Text notes with rich formatting
|
| 35 |
-
- Image cropping, flipping, grouping, desaturate
|
| 36 |
-
- Minimap for navigation
|
| 37 |
-
- StarterHub for project management
|
| 38 |
-
- Always-on-top + click-through + opacity control
|
| 39 |
-
|
| 40 |
-
## Files archived
|
| 41 |
-
The previous `artifacts/musealpha-production.zip` contains the browser-first build.
|
| 42 |
-
The `ui-prototype/` directory contains the browser-first prototype.
|
| 43 |
-
The source in `src/` and `src-tauri/` is now being replaced with the board-first architecture from `uiprototype2/`.
|
|
|
|
| 1 |
+
# Browser-First Era Archive
|
| 2 |
+
|
| 3 |
+
This document marks the architectural pivot from **browser-first** (Muse Alpha v0.2) to **board-first** (RefStudio v1.0).
|
| 4 |
+
|
| 5 |
+
## What was the browser-first approach?
|
| 6 |
+
- Main workspace was a web browser with tabs
|
| 7 |
+
- Sidebar navigation between Home/Browser/Library/Board/Settings views
|
| 8 |
+
- Board was a secondary view accessed from the sidebar
|
| 9 |
+
- Image hover overlay injected into browser tabs for saving references
|
| 10 |
+
- Ad-blocking engine for browsing comfort
|
| 11 |
+
|
| 12 |
+
## Why the pivot?
|
| 13 |
+
The core use case is **reference organization**, not web browsing. Artists need:
|
| 14 |
+
- An infinite canvas as the PRIMARY workspace (like PureRef)
|
| 15 |
+
- Browser as a TOOL (embedded panel) to find references, not the main UI
|
| 16 |
+
- Zero-friction image capture from browser → canvas
|
| 17 |
+
- Always-on-top mode for overlaying references while working in art apps
|
| 18 |
+
- Click-through transparency for tracing
|
| 19 |
+
|
| 20 |
+
## What's preserved from browser-first era?
|
| 21 |
+
- Rust adblock engine (`adblock/` module)
|
| 22 |
+
- Tauri child webview architecture for embedded browser
|
| 23 |
+
- Image download + library persistence (`library.rs`, `persistence.rs`)
|
| 24 |
+
- Board item CRUD (`board.rs`)
|
| 25 |
+
- SQLite migrations
|
| 26 |
+
- Credentials/stronghold setup
|
| 27 |
+
|
| 28 |
+
## New architecture (board-first / RefStudio)
|
| 29 |
+
- Infinite canvas is the main and only workspace
|
| 30 |
+
- Browser panel slides in from the right as a capture tool
|
| 31 |
+
- Library panel slides in from the left for asset management
|
| 32 |
+
- Three.js GPU-accelerated image rendering with shaders
|
| 33 |
+
- Annotations (freehand drawing) directly on canvas
|
| 34 |
+
- Text notes with rich formatting
|
| 35 |
+
- Image cropping, flipping, grouping, desaturate
|
| 36 |
+
- Minimap for navigation
|
| 37 |
+
- StarterHub for project management
|
| 38 |
+
- Always-on-top + click-through + opacity control
|
| 39 |
+
|
| 40 |
+
## Files archived
|
| 41 |
+
The previous `artifacts/musealpha-production.zip` contains the browser-first build.
|
| 42 |
+
The `ui-prototype/` directory contains the browser-first prototype.
|
| 43 |
+
The source in `src/` and `src-tauri/` is now being replaced with the board-first architecture from `uiprototype2/`.
|
docs/MUSE_SRS_v3.md
CHANGED
|
@@ -1,205 +1,205 @@
|
|
| 1 |
-
# MUSE — Master Software Requirements Specification v3.0
|
| 2 |
-
### Creative Browser · Tauri v2 + Rust · Complete Product Definition
|
| 3 |
-
|
| 4 |
-
> Canonical v3 requirements document supplied by the product owner in the implementation prompt.
|
| 5 |
-
|
| 6 |
-
This document defines the full Muse product vision, architecture, browser fundamentals, ad-block v3, creative modules, artist-exclusive features, UI/UX system, business model, roadmap, and non-goals.
|
| 7 |
-
|
| 8 |
-
## Document map
|
| 9 |
-
|
| 10 |
-
Incorporates and supersedes:
|
| 11 |
-
- SRS v1.0 (original vision + market research)
|
| 12 |
-
- SRS v2.0 (deep UX/UI + themes + ad-block)
|
| 13 |
-
- 01-DATABASE-ARCHITECTURE.md
|
| 14 |
-
- 02-ADBLOCK-INTEGRATION.md
|
| 15 |
-
- 03-RUST-ARCHITECTURE.md
|
| 16 |
-
- 04-USER-RESEARCH.md
|
| 17 |
-
- 05-PLATFORM-SPECIFIC.md
|
| 18 |
-
- 06-MOBILE-TABLET-UX.md
|
| 19 |
-
- 07-ALGORITHMS-THIRDPARTY.md
|
| 20 |
-
- 08-UI-UX-COMPLETE.md
|
| 21 |
-
|
| 22 |
-
## Core mission
|
| 23 |
-
|
| 24 |
-
Muse is the first browser designed specifically for the visual artist's creative loop:
|
| 25 |
-
|
| 26 |
-
```text
|
| 27 |
-
Discover → Collect → Organize → Reference → Create → Repeat
|
| 28 |
-
```
|
| 29 |
-
|
| 30 |
-
Muse replaces the fragmented five-app workflow:
|
| 31 |
-
- Chrome/Firefox: full browser
|
| 32 |
-
- PureRef: Board module / infinite reference canvas
|
| 33 |
-
- Eagle App: Library module
|
| 34 |
-
- Pinterest/Are.na: Boards + Library without algorithm/account/subscription
|
| 35 |
-
- Standalone color picker: integrated extraction, picker, search, export
|
| 36 |
-
|
| 37 |
-
## Locked non-goals
|
| 38 |
-
|
| 39 |
-
- No AI image generation
|
| 40 |
-
- No subscription model
|
| 41 |
-
- No hosted cloud storage
|
| 42 |
-
- No social feed or algorithm
|
| 43 |
-
- No drawing tools
|
| 44 |
-
- No real-time collaboration for v1 scope
|
| 45 |
-
- No telemetry without explicit consent
|
| 46 |
-
- No bundled Chromium
|
| 47 |
-
- No mandatory account
|
| 48 |
-
|
| 49 |
-
## Architecture summary
|
| 50 |
-
|
| 51 |
-
Stack:
|
| 52 |
-
- Tauri v2 + Rust core
|
| 53 |
-
- SolidJS shell frontend
|
| 54 |
-
- System WebViews for browser tabs
|
| 55 |
-
- SQLite + WAL for local data
|
| 56 |
-
- Stronghold for encrypted credentials
|
| 57 |
-
- `tauri-plugin-store` for settings
|
| 58 |
-
- `brave/adblock-rust` for filter-rule engine
|
| 59 |
-
|
| 60 |
-
Rust module plan:
|
| 61 |
-
- `browser/` tab lifecycle, navigation, injection, tab sleep, find
|
| 62 |
-
- `adblock/` engine, filter lists, user rules, subscriptions, stats, cosmetic injection
|
| 63 |
-
- `library/` SQLite CRUD, thumbnails, color extraction, dedupe, FTS5 search, smart folders
|
| 64 |
-
- `board/` infinite canvas, autosave, items, export, annotations
|
| 65 |
-
- `credentials/` vault, autofill, generator
|
| 66 |
-
- `downloads/`, `cookies/`, `sessions/`, `permissions/`, `privacy/`, `color/`, `workspace/`
|
| 67 |
-
|
| 68 |
-
## Database summary
|
| 69 |
-
|
| 70 |
-
SQLite with startup PRAGMAs:
|
| 71 |
-
|
| 72 |
-
```sql
|
| 73 |
-
PRAGMA journal_mode = WAL;
|
| 74 |
-
PRAGMA synchronous = NORMAL;
|
| 75 |
-
PRAGMA mmap_size = 268435456;
|
| 76 |
-
PRAGMA cache_size = -16384;
|
| 77 |
-
PRAGMA temp_store = MEMORY;
|
| 78 |
-
PRAGMA foreign_keys = ON;
|
| 79 |
-
PRAGMA busy_timeout = 5000;
|
| 80 |
-
PRAGMA auto_vacuum = INCREMENTAL;
|
| 81 |
-
```
|
| 82 |
-
|
| 83 |
-
New v3 tables include:
|
| 84 |
-
- `sessions`, `session_tabs`
|
| 85 |
-
- `credentials` pointers to Stronghold keys
|
| 86 |
-
- `downloads`
|
| 87 |
-
- `cookie_rules`
|
| 88 |
-
- `site_permissions`
|
| 89 |
-
- `site_zoom`
|
| 90 |
-
- `adblock_subscriptions`
|
| 91 |
-
- `adblock_user_rules`
|
| 92 |
-
- `board_annotations`
|
| 93 |
-
- `study_sessions`
|
| 94 |
-
|
| 95 |
-
## Browser fundamentals covered
|
| 96 |
-
|
| 97 |
-
- Cookie management
|
| 98 |
-
- Password and credential manager
|
| 99 |
-
- Download manager
|
| 100 |
-
- Tab sleep
|
| 101 |
-
- Session management
|
| 102 |
-
- Find in page
|
| 103 |
-
- Per-site zoom memory
|
| 104 |
-
- Permission system
|
| 105 |
-
- HTTPS-first and safe browsing without Google URL API
|
| 106 |
-
- DNS over HTTPS
|
| 107 |
-
- Print
|
| 108 |
-
- Autofill
|
| 109 |
-
- WebRTC and fingerprint protection
|
| 110 |
-
- Offline/error pages
|
| 111 |
-
- Media autoplay policy
|
| 112 |
-
- Certificate/security info
|
| 113 |
-
|
| 114 |
-
## Ad-block v3
|
| 115 |
-
|
| 116 |
-
Hybrid design in v3:
|
| 117 |
-
- Navigation blocking
|
| 118 |
-
- Native network blocking where available
|
| 119 |
-
- Cosmetic filtering
|
| 120 |
-
- Built-in multi-list subscriptions
|
| 121 |
-
- Custom uBlock-syntax rules
|
| 122 |
-
- Statistics and transparency
|
| 123 |
-
- Cookie consent auto-denial
|
| 124 |
-
|
| 125 |
-
Built-in lists:
|
| 126 |
-
- EasyList
|
| 127 |
-
- EasyPrivacy
|
| 128 |
-
- uBlock filters
|
| 129 |
-
- uBlock Unbreak
|
| 130 |
-
- Fanboy Annoyances
|
| 131 |
-
- Peter Lowe
|
| 132 |
-
- Malware Domains
|
| 133 |
-
- AdGuard Base
|
| 134 |
-
- AdGuard Social
|
| 135 |
-
- IndianList
|
| 136 |
-
|
| 137 |
-
## Core creative modules
|
| 138 |
-
|
| 139 |
-
- Home Page
|
| 140 |
-
- URL Bar System
|
| 141 |
-
- Sidebar & Tab System
|
| 142 |
-
- Library Module
|
| 143 |
-
- Board Module / PureRef replacement
|
| 144 |
-
- Split View System
|
| 145 |
-
- Color Tools
|
| 146 |
-
- Image Hover Overlay
|
| 147 |
-
|
| 148 |
-
## Artist-exclusive v3 modules
|
| 149 |
-
|
| 150 |
-
- Quick Study Mode
|
| 151 |
-
- Board annotations/markup
|
| 152 |
-
- Rotation, flip, light table, overlays
|
| 153 |
-
- Proportion and grid overlays
|
| 154 |
-
- Source site intelligence profiles
|
| 155 |
-
- Library timeline view
|
| 156 |
-
- Color export for artist apps
|
| 157 |
-
- Reference sheet export
|
| 158 |
-
- Workspace sessions for projects
|
| 159 |
-
|
| 160 |
-
## UI/UX system
|
| 161 |
-
|
| 162 |
-
Theme presets:
|
| 163 |
-
- Dusk
|
| 164 |
-
- Parchment
|
| 165 |
-
- Midnight
|
| 166 |
-
- Studio
|
| 167 |
-
- Moss
|
| 168 |
-
- Rose
|
| 169 |
-
- Obsidian
|
| 170 |
-
- Linen
|
| 171 |
-
|
| 172 |
-
Typography:
|
| 173 |
-
- Geist UI
|
| 174 |
-
- Lora reader body
|
| 175 |
-
- Geist Mono for code/dimensions
|
| 176 |
-
|
| 177 |
-
Onboarding:
|
| 178 |
-
1. Welcome
|
| 179 |
-
2. Make it yours (name + theme)
|
| 180 |
-
3. Name first workspace
|
| 181 |
-
|
| 182 |
-
Keyboard/command palette includes standard browser shortcuts plus Muse creative shortcuts.
|
| 183 |
-
|
| 184 |
-
## Business model
|
| 185 |
-
|
| 186 |
-
- Free tier forever
|
| 187 |
-
- Muse one-time purchase
|
| 188 |
-
- Muse Pro one-time upgrade
|
| 189 |
-
- Pay-What-You-Want launch window
|
| 190 |
-
- No subscription
|
| 191 |
-
|
| 192 |
-
## Roadmap
|
| 193 |
-
|
| 194 |
-
- Phase 0: Foundation
|
| 195 |
-
- Phase 1: Browser basics
|
| 196 |
-
- Phase 2: Browser fundamentals complete
|
| 197 |
-
- Phase 3: Creative core
|
| 198 |
-
- Phase 4: Power features
|
| 199 |
-
- Phase 5: Ship Windows
|
| 200 |
-
- Phase 6: Cross-platform
|
| 201 |
-
- Phase 7: Ecosystem
|
| 202 |
-
|
| 203 |
-
## Note
|
| 204 |
-
|
| 205 |
-
The original full v3 text was supplied in-chat and is treated as the requirements source for this repository. Phase 0 implementation begins from the v4 implementation guide while preserving these v3 product requirements.
|
|
|
|
| 1 |
+
# MUSE — Master Software Requirements Specification v3.0
|
| 2 |
+
### Creative Browser · Tauri v2 + Rust · Complete Product Definition
|
| 3 |
+
|
| 4 |
+
> Canonical v3 requirements document supplied by the product owner in the implementation prompt.
|
| 5 |
+
|
| 6 |
+
This document defines the full Muse product vision, architecture, browser fundamentals, ad-block v3, creative modules, artist-exclusive features, UI/UX system, business model, roadmap, and non-goals.
|
| 7 |
+
|
| 8 |
+
## Document map
|
| 9 |
+
|
| 10 |
+
Incorporates and supersedes:
|
| 11 |
+
- SRS v1.0 (original vision + market research)
|
| 12 |
+
- SRS v2.0 (deep UX/UI + themes + ad-block)
|
| 13 |
+
- 01-DATABASE-ARCHITECTURE.md
|
| 14 |
+
- 02-ADBLOCK-INTEGRATION.md
|
| 15 |
+
- 03-RUST-ARCHITECTURE.md
|
| 16 |
+
- 04-USER-RESEARCH.md
|
| 17 |
+
- 05-PLATFORM-SPECIFIC.md
|
| 18 |
+
- 06-MOBILE-TABLET-UX.md
|
| 19 |
+
- 07-ALGORITHMS-THIRDPARTY.md
|
| 20 |
+
- 08-UI-UX-COMPLETE.md
|
| 21 |
+
|
| 22 |
+
## Core mission
|
| 23 |
+
|
| 24 |
+
Muse is the first browser designed specifically for the visual artist's creative loop:
|
| 25 |
+
|
| 26 |
+
```text
|
| 27 |
+
Discover → Collect → Organize → Reference → Create → Repeat
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
Muse replaces the fragmented five-app workflow:
|
| 31 |
+
- Chrome/Firefox: full browser
|
| 32 |
+
- PureRef: Board module / infinite reference canvas
|
| 33 |
+
- Eagle App: Library module
|
| 34 |
+
- Pinterest/Are.na: Boards + Library without algorithm/account/subscription
|
| 35 |
+
- Standalone color picker: integrated extraction, picker, search, export
|
| 36 |
+
|
| 37 |
+
## Locked non-goals
|
| 38 |
+
|
| 39 |
+
- No AI image generation
|
| 40 |
+
- No subscription model
|
| 41 |
+
- No hosted cloud storage
|
| 42 |
+
- No social feed or algorithm
|
| 43 |
+
- No drawing tools
|
| 44 |
+
- No real-time collaboration for v1 scope
|
| 45 |
+
- No telemetry without explicit consent
|
| 46 |
+
- No bundled Chromium
|
| 47 |
+
- No mandatory account
|
| 48 |
+
|
| 49 |
+
## Architecture summary
|
| 50 |
+
|
| 51 |
+
Stack:
|
| 52 |
+
- Tauri v2 + Rust core
|
| 53 |
+
- SolidJS shell frontend
|
| 54 |
+
- System WebViews for browser tabs
|
| 55 |
+
- SQLite + WAL for local data
|
| 56 |
+
- Stronghold for encrypted credentials
|
| 57 |
+
- `tauri-plugin-store` for settings
|
| 58 |
+
- `brave/adblock-rust` for filter-rule engine
|
| 59 |
+
|
| 60 |
+
Rust module plan:
|
| 61 |
+
- `browser/` tab lifecycle, navigation, injection, tab sleep, find
|
| 62 |
+
- `adblock/` engine, filter lists, user rules, subscriptions, stats, cosmetic injection
|
| 63 |
+
- `library/` SQLite CRUD, thumbnails, color extraction, dedupe, FTS5 search, smart folders
|
| 64 |
+
- `board/` infinite canvas, autosave, items, export, annotations
|
| 65 |
+
- `credentials/` vault, autofill, generator
|
| 66 |
+
- `downloads/`, `cookies/`, `sessions/`, `permissions/`, `privacy/`, `color/`, `workspace/`
|
| 67 |
+
|
| 68 |
+
## Database summary
|
| 69 |
+
|
| 70 |
+
SQLite with startup PRAGMAs:
|
| 71 |
+
|
| 72 |
+
```sql
|
| 73 |
+
PRAGMA journal_mode = WAL;
|
| 74 |
+
PRAGMA synchronous = NORMAL;
|
| 75 |
+
PRAGMA mmap_size = 268435456;
|
| 76 |
+
PRAGMA cache_size = -16384;
|
| 77 |
+
PRAGMA temp_store = MEMORY;
|
| 78 |
+
PRAGMA foreign_keys = ON;
|
| 79 |
+
PRAGMA busy_timeout = 5000;
|
| 80 |
+
PRAGMA auto_vacuum = INCREMENTAL;
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
New v3 tables include:
|
| 84 |
+
- `sessions`, `session_tabs`
|
| 85 |
+
- `credentials` pointers to Stronghold keys
|
| 86 |
+
- `downloads`
|
| 87 |
+
- `cookie_rules`
|
| 88 |
+
- `site_permissions`
|
| 89 |
+
- `site_zoom`
|
| 90 |
+
- `adblock_subscriptions`
|
| 91 |
+
- `adblock_user_rules`
|
| 92 |
+
- `board_annotations`
|
| 93 |
+
- `study_sessions`
|
| 94 |
+
|
| 95 |
+
## Browser fundamentals covered
|
| 96 |
+
|
| 97 |
+
- Cookie management
|
| 98 |
+
- Password and credential manager
|
| 99 |
+
- Download manager
|
| 100 |
+
- Tab sleep
|
| 101 |
+
- Session management
|
| 102 |
+
- Find in page
|
| 103 |
+
- Per-site zoom memory
|
| 104 |
+
- Permission system
|
| 105 |
+
- HTTPS-first and safe browsing without Google URL API
|
| 106 |
+
- DNS over HTTPS
|
| 107 |
+
- Print
|
| 108 |
+
- Autofill
|
| 109 |
+
- WebRTC and fingerprint protection
|
| 110 |
+
- Offline/error pages
|
| 111 |
+
- Media autoplay policy
|
| 112 |
+
- Certificate/security info
|
| 113 |
+
|
| 114 |
+
## Ad-block v3
|
| 115 |
+
|
| 116 |
+
Hybrid design in v3:
|
| 117 |
+
- Navigation blocking
|
| 118 |
+
- Native network blocking where available
|
| 119 |
+
- Cosmetic filtering
|
| 120 |
+
- Built-in multi-list subscriptions
|
| 121 |
+
- Custom uBlock-syntax rules
|
| 122 |
+
- Statistics and transparency
|
| 123 |
+
- Cookie consent auto-denial
|
| 124 |
+
|
| 125 |
+
Built-in lists:
|
| 126 |
+
- EasyList
|
| 127 |
+
- EasyPrivacy
|
| 128 |
+
- uBlock filters
|
| 129 |
+
- uBlock Unbreak
|
| 130 |
+
- Fanboy Annoyances
|
| 131 |
+
- Peter Lowe
|
| 132 |
+
- Malware Domains
|
| 133 |
+
- AdGuard Base
|
| 134 |
+
- AdGuard Social
|
| 135 |
+
- IndianList
|
| 136 |
+
|
| 137 |
+
## Core creative modules
|
| 138 |
+
|
| 139 |
+
- Home Page
|
| 140 |
+
- URL Bar System
|
| 141 |
+
- Sidebar & Tab System
|
| 142 |
+
- Library Module
|
| 143 |
+
- Board Module / PureRef replacement
|
| 144 |
+
- Split View System
|
| 145 |
+
- Color Tools
|
| 146 |
+
- Image Hover Overlay
|
| 147 |
+
|
| 148 |
+
## Artist-exclusive v3 modules
|
| 149 |
+
|
| 150 |
+
- Quick Study Mode
|
| 151 |
+
- Board annotations/markup
|
| 152 |
+
- Rotation, flip, light table, overlays
|
| 153 |
+
- Proportion and grid overlays
|
| 154 |
+
- Source site intelligence profiles
|
| 155 |
+
- Library timeline view
|
| 156 |
+
- Color export for artist apps
|
| 157 |
+
- Reference sheet export
|
| 158 |
+
- Workspace sessions for projects
|
| 159 |
+
|
| 160 |
+
## UI/UX system
|
| 161 |
+
|
| 162 |
+
Theme presets:
|
| 163 |
+
- Dusk
|
| 164 |
+
- Parchment
|
| 165 |
+
- Midnight
|
| 166 |
+
- Studio
|
| 167 |
+
- Moss
|
| 168 |
+
- Rose
|
| 169 |
+
- Obsidian
|
| 170 |
+
- Linen
|
| 171 |
+
|
| 172 |
+
Typography:
|
| 173 |
+
- Geist UI
|
| 174 |
+
- Lora reader body
|
| 175 |
+
- Geist Mono for code/dimensions
|
| 176 |
+
|
| 177 |
+
Onboarding:
|
| 178 |
+
1. Welcome
|
| 179 |
+
2. Make it yours (name + theme)
|
| 180 |
+
3. Name first workspace
|
| 181 |
+
|
| 182 |
+
Keyboard/command palette includes standard browser shortcuts plus Muse creative shortcuts.
|
| 183 |
+
|
| 184 |
+
## Business model
|
| 185 |
+
|
| 186 |
+
- Free tier forever
|
| 187 |
+
- Muse one-time purchase
|
| 188 |
+
- Muse Pro one-time upgrade
|
| 189 |
+
- Pay-What-You-Want launch window
|
| 190 |
+
- No subscription
|
| 191 |
+
|
| 192 |
+
## Roadmap
|
| 193 |
+
|
| 194 |
+
- Phase 0: Foundation
|
| 195 |
+
- Phase 1: Browser basics
|
| 196 |
+
- Phase 2: Browser fundamentals complete
|
| 197 |
+
- Phase 3: Creative core
|
| 198 |
+
- Phase 4: Power features
|
| 199 |
+
- Phase 5: Ship Windows
|
| 200 |
+
- Phase 6: Cross-platform
|
| 201 |
+
- Phase 7: Ecosystem
|
| 202 |
+
|
| 203 |
+
## Note
|
| 204 |
+
|
| 205 |
+
The original full v3 text was supplied in-chat and is treated as the requirements source for this repository. Phase 0 implementation begins from the v4 implementation guide while preserving these v3 product requirements.
|
docs/MUSE_SRS_v4.md
CHANGED
|
@@ -1,170 +1,170 @@
|
|
| 1 |
-
# MUSE — Master SRS v4.0
|
| 2 |
-
## Creative Browser · Tauri v2 + Rust · Complete Implementation Guide
|
| 3 |
-
|
| 4 |
-
> One window. Multiple child webviews. One ad-block engine. All platforms.
|
| 5 |
-
|
| 6 |
-
This implementation guide supersedes earlier architecture/code notes for v1.0 implementation sequencing.
|
| 7 |
-
|
| 8 |
-
## Critical architecture decisions
|
| 9 |
-
|
| 10 |
-
### 1. Single OS window + child webviews for tabs
|
| 11 |
-
|
| 12 |
-
Muse is one OS window containing:
|
| 13 |
-
- Shell WebView: SolidJS UI (titlebar, sidebar, URL bar, home, library, boards)
|
| 14 |
-
- Child tab WebViews: one per browser tab
|
| 15 |
-
|
| 16 |
-
Implemented later with `window.add_child()` behind Tauri's `unstable` feature. Phase 0 does **not** use child WebViews yet.
|
| 17 |
-
|
| 18 |
-
### 2. Cross-platform ad-block = Rust engine + JS injection
|
| 19 |
-
|
| 20 |
-
The v4 guide deliberately removes platform-native WebView2/WKContentRuleList/WebKitGTK-specific blocking from the v1 cross-platform plan.
|
| 21 |
-
|
| 22 |
-
Ad-block layers:
|
| 23 |
-
- Layer 0: Navigation handler in Rust
|
| 24 |
-
- Layer 1: JS fetch/XHR override
|
| 25 |
-
- Layer 2: DOM CSP injection
|
| 26 |
-
- Layer 3: Cosmetic CSS injection
|
| 27 |
-
- Layer 4: Scriptlet injection
|
| 28 |
-
|
| 29 |
-
### 3. `unstable` feature is acceptable for desktop tabs
|
| 30 |
-
|
| 31 |
-
`tauri = { version = "2", features = ["unstable"] }` is planned once Phase 1 implements child tab WebViews.
|
| 32 |
-
|
| 33 |
-
### 4. Mobile tab model differs
|
| 34 |
-
|
| 35 |
-
Mobile uses conceptual tab state plus `WebviewWindow` until Tauri supports child webviews on mobile. Desktop Windows remains v1 target.
|
| 36 |
-
|
| 37 |
-
## Repository structure target
|
| 38 |
-
|
| 39 |
-
```text
|
| 40 |
-
muse/
|
| 41 |
-
├── package.json
|
| 42 |
-
├── vite.config.ts
|
| 43 |
-
├── index.html
|
| 44 |
-
├── src/
|
| 45 |
-
│ ├── index.tsx
|
| 46 |
-
│ ├── App.tsx
|
| 47 |
-
│ ├── components/
|
| 48 |
-
│ ├── store/
|
| 49 |
-
│ └── styles/
|
| 50 |
-
└── src-tauri/
|
| 51 |
-
├── Cargo.toml
|
| 52 |
-
├── build.rs
|
| 53 |
-
├── tauri.conf.json
|
| 54 |
-
├── capabilities/default.json
|
| 55 |
-
├── migrations/
|
| 56 |
-
└── src/
|
| 57 |
-
├── main.rs
|
| 58 |
-
├── lib.rs
|
| 59 |
-
├── state.rs
|
| 60 |
-
├── settings.rs
|
| 61 |
-
├── browser/
|
| 62 |
-
├── adblock/
|
| 63 |
-
├── library/
|
| 64 |
-
├── board/
|
| 65 |
-
├── credentials/
|
| 66 |
-
├── downloads/
|
| 67 |
-
├── sessions/
|
| 68 |
-
└── db/
|
| 69 |
-
```
|
| 70 |
-
|
| 71 |
-
## Phase 0 implementation scope
|
| 72 |
-
|
| 73 |
-
Deliverable: running app with custom titlebar, theme, and sidebar navigation.
|
| 74 |
-
|
| 75 |
-
User can:
|
| 76 |
-
- Open Muse on Windows
|
| 77 |
-
- See Dusk theme, custom titlebar, sidebar
|
| 78 |
-
- Click sidebar icons (Library/Board placeholders)
|
| 79 |
-
- See Home page with greeting and static URL bar
|
| 80 |
-
- Switch all 8 themes
|
| 81 |
-
- Complete onboarding in under 60 seconds
|
| 82 |
-
|
| 83 |
-
Not included yet:
|
| 84 |
-
- Real browser tabs
|
| 85 |
-
- Ad-block
|
| 86 |
-
- Library/Board functionality
|
| 87 |
-
|
| 88 |
-
Phase 0 work items:
|
| 89 |
-
- SolidJS Tauri app skeleton
|
| 90 |
-
- Tauri v2 `Cargo.toml`
|
| 91 |
-
- `tauri.conf.json` with undecorated window
|
| 92 |
-
- Capabilities file
|
| 93 |
-
- Custom titlebar with window controls
|
| 94 |
-
- Sidebar shell
|
| 95 |
-
- Home page shell
|
| 96 |
-
- Theme token CSS for 8 themes
|
| 97 |
-
- App state module
|
| 98 |
-
- SQLite init and migration
|
| 99 |
-
- Tauri Store settings
|
| 100 |
-
- Onboarding flow
|
| 101 |
-
|
| 102 |
-
## Phase 1 target
|
| 103 |
-
|
| 104 |
-
Real multi-tab browser:
|
| 105 |
-
- `TabManager`
|
| 106 |
-
- `window.add_child()` child WebViews
|
| 107 |
-
- tab create/close/switch/navigate
|
| 108 |
-
- sidebar tab list
|
| 109 |
-
- URL bar navigation
|
| 110 |
-
- title/favicon observer
|
| 111 |
-
- resize handling
|
| 112 |
-
- per-site zoom
|
| 113 |
-
|
| 114 |
-
## Phase 2 target
|
| 115 |
-
|
| 116 |
-
Ad-block and privacy:
|
| 117 |
-
- adblock-rust engine
|
| 118 |
-
- 10 bundled lists
|
| 119 |
-
- JS network-request interception
|
| 120 |
-
- cookie consent scriptlet
|
| 121 |
-
- WebRTC protection
|
| 122 |
-
- canvas noise
|
| 123 |
-
- navigation blocking
|
| 124 |
-
- cosmetic injection
|
| 125 |
-
- updater
|
| 126 |
-
- allowlist
|
| 127 |
-
- shield UI
|
| 128 |
-
- tab sleep
|
| 129 |
-
- HTTPS-first
|
| 130 |
-
- Stronghold password vault basics
|
| 131 |
-
|
| 132 |
-
## Phase 3 target
|
| 133 |
-
|
| 134 |
-
Library + Board core:
|
| 135 |
-
- image hover overlay
|
| 136 |
-
- save-to-library
|
| 137 |
-
- library search/grid/detail
|
| 138 |
-
- board infinite canvas
|
| 139 |
-
- board item CRUD/autosave
|
| 140 |
-
- palette extraction
|
| 141 |
-
- download manager
|
| 142 |
-
- sessions auto-save
|
| 143 |
-
|
| 144 |
-
## Phase 4 target
|
| 145 |
-
|
| 146 |
-
Artist features and polish:
|
| 147 |
-
- Quick Study Mode
|
| 148 |
-
- annotations
|
| 149 |
-
- rotate/flip/light table
|
| 150 |
-
- overlays
|
| 151 |
-
- timeline
|
| 152 |
-
- source profiles
|
| 153 |
-
- export formats
|
| 154 |
-
- reference sheets
|
| 155 |
-
- session manager UI
|
| 156 |
-
- command palette
|
| 157 |
-
- permissions/cert/offline/print/DoH
|
| 158 |
-
|
| 159 |
-
## Phase 5 target
|
| 160 |
-
|
| 161 |
-
Windows v1 shipping:
|
| 162 |
-
- NSIS installer
|
| 163 |
-
- updater
|
| 164 |
-
- code signing
|
| 165 |
-
- optional crash reporting
|
| 166 |
-
- performance audit
|
| 167 |
-
|
| 168 |
-
## Current implementation note
|
| 169 |
-
|
| 170 |
-
The committed code implements the Phase 0 scope only. Phase 1+ modules are represented as directory placeholders and comments where useful, but no tab WebViews are created in Phase 0.
|
|
|
|
| 1 |
+
# MUSE — Master SRS v4.0
|
| 2 |
+
## Creative Browser · Tauri v2 + Rust · Complete Implementation Guide
|
| 3 |
+
|
| 4 |
+
> One window. Multiple child webviews. One ad-block engine. All platforms.
|
| 5 |
+
|
| 6 |
+
This implementation guide supersedes earlier architecture/code notes for v1.0 implementation sequencing.
|
| 7 |
+
|
| 8 |
+
## Critical architecture decisions
|
| 9 |
+
|
| 10 |
+
### 1. Single OS window + child webviews for tabs
|
| 11 |
+
|
| 12 |
+
Muse is one OS window containing:
|
| 13 |
+
- Shell WebView: SolidJS UI (titlebar, sidebar, URL bar, home, library, boards)
|
| 14 |
+
- Child tab WebViews: one per browser tab
|
| 15 |
+
|
| 16 |
+
Implemented later with `window.add_child()` behind Tauri's `unstable` feature. Phase 0 does **not** use child WebViews yet.
|
| 17 |
+
|
| 18 |
+
### 2. Cross-platform ad-block = Rust engine + JS injection
|
| 19 |
+
|
| 20 |
+
The v4 guide deliberately removes platform-native WebView2/WKContentRuleList/WebKitGTK-specific blocking from the v1 cross-platform plan.
|
| 21 |
+
|
| 22 |
+
Ad-block layers:
|
| 23 |
+
- Layer 0: Navigation handler in Rust
|
| 24 |
+
- Layer 1: JS fetch/XHR override
|
| 25 |
+
- Layer 2: DOM CSP injection
|
| 26 |
+
- Layer 3: Cosmetic CSS injection
|
| 27 |
+
- Layer 4: Scriptlet injection
|
| 28 |
+
|
| 29 |
+
### 3. `unstable` feature is acceptable for desktop tabs
|
| 30 |
+
|
| 31 |
+
`tauri = { version = "2", features = ["unstable"] }` is planned once Phase 1 implements child tab WebViews.
|
| 32 |
+
|
| 33 |
+
### 4. Mobile tab model differs
|
| 34 |
+
|
| 35 |
+
Mobile uses conceptual tab state plus `WebviewWindow` until Tauri supports child webviews on mobile. Desktop Windows remains v1 target.
|
| 36 |
+
|
| 37 |
+
## Repository structure target
|
| 38 |
+
|
| 39 |
+
```text
|
| 40 |
+
muse/
|
| 41 |
+
├── package.json
|
| 42 |
+
├── vite.config.ts
|
| 43 |
+
├── index.html
|
| 44 |
+
├── src/
|
| 45 |
+
│ ├── index.tsx
|
| 46 |
+
│ ├── App.tsx
|
| 47 |
+
│ ├── components/
|
| 48 |
+
│ ├── store/
|
| 49 |
+
│ └── styles/
|
| 50 |
+
└── src-tauri/
|
| 51 |
+
├── Cargo.toml
|
| 52 |
+
├── build.rs
|
| 53 |
+
├── tauri.conf.json
|
| 54 |
+
├── capabilities/default.json
|
| 55 |
+
├── migrations/
|
| 56 |
+
└── src/
|
| 57 |
+
├── main.rs
|
| 58 |
+
├── lib.rs
|
| 59 |
+
├── state.rs
|
| 60 |
+
├── settings.rs
|
| 61 |
+
├── browser/
|
| 62 |
+
├── adblock/
|
| 63 |
+
├── library/
|
| 64 |
+
├── board/
|
| 65 |
+
├── credentials/
|
| 66 |
+
├── downloads/
|
| 67 |
+
├── sessions/
|
| 68 |
+
└── db/
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Phase 0 implementation scope
|
| 72 |
+
|
| 73 |
+
Deliverable: running app with custom titlebar, theme, and sidebar navigation.
|
| 74 |
+
|
| 75 |
+
User can:
|
| 76 |
+
- Open Muse on Windows
|
| 77 |
+
- See Dusk theme, custom titlebar, sidebar
|
| 78 |
+
- Click sidebar icons (Library/Board placeholders)
|
| 79 |
+
- See Home page with greeting and static URL bar
|
| 80 |
+
- Switch all 8 themes
|
| 81 |
+
- Complete onboarding in under 60 seconds
|
| 82 |
+
|
| 83 |
+
Not included yet:
|
| 84 |
+
- Real browser tabs
|
| 85 |
+
- Ad-block
|
| 86 |
+
- Library/Board functionality
|
| 87 |
+
|
| 88 |
+
Phase 0 work items:
|
| 89 |
+
- SolidJS Tauri app skeleton
|
| 90 |
+
- Tauri v2 `Cargo.toml`
|
| 91 |
+
- `tauri.conf.json` with undecorated window
|
| 92 |
+
- Capabilities file
|
| 93 |
+
- Custom titlebar with window controls
|
| 94 |
+
- Sidebar shell
|
| 95 |
+
- Home page shell
|
| 96 |
+
- Theme token CSS for 8 themes
|
| 97 |
+
- App state module
|
| 98 |
+
- SQLite init and migration
|
| 99 |
+
- Tauri Store settings
|
| 100 |
+
- Onboarding flow
|
| 101 |
+
|
| 102 |
+
## Phase 1 target
|
| 103 |
+
|
| 104 |
+
Real multi-tab browser:
|
| 105 |
+
- `TabManager`
|
| 106 |
+
- `window.add_child()` child WebViews
|
| 107 |
+
- tab create/close/switch/navigate
|
| 108 |
+
- sidebar tab list
|
| 109 |
+
- URL bar navigation
|
| 110 |
+
- title/favicon observer
|
| 111 |
+
- resize handling
|
| 112 |
+
- per-site zoom
|
| 113 |
+
|
| 114 |
+
## Phase 2 target
|
| 115 |
+
|
| 116 |
+
Ad-block and privacy:
|
| 117 |
+
- adblock-rust engine
|
| 118 |
+
- 10 bundled lists
|
| 119 |
+
- JS network-request interception
|
| 120 |
+
- cookie consent scriptlet
|
| 121 |
+
- WebRTC protection
|
| 122 |
+
- canvas noise
|
| 123 |
+
- navigation blocking
|
| 124 |
+
- cosmetic injection
|
| 125 |
+
- updater
|
| 126 |
+
- allowlist
|
| 127 |
+
- shield UI
|
| 128 |
+
- tab sleep
|
| 129 |
+
- HTTPS-first
|
| 130 |
+
- Stronghold password vault basics
|
| 131 |
+
|
| 132 |
+
## Phase 3 target
|
| 133 |
+
|
| 134 |
+
Library + Board core:
|
| 135 |
+
- image hover overlay
|
| 136 |
+
- save-to-library
|
| 137 |
+
- library search/grid/detail
|
| 138 |
+
- board infinite canvas
|
| 139 |
+
- board item CRUD/autosave
|
| 140 |
+
- palette extraction
|
| 141 |
+
- download manager
|
| 142 |
+
- sessions auto-save
|
| 143 |
+
|
| 144 |
+
## Phase 4 target
|
| 145 |
+
|
| 146 |
+
Artist features and polish:
|
| 147 |
+
- Quick Study Mode
|
| 148 |
+
- annotations
|
| 149 |
+
- rotate/flip/light table
|
| 150 |
+
- overlays
|
| 151 |
+
- timeline
|
| 152 |
+
- source profiles
|
| 153 |
+
- export formats
|
| 154 |
+
- reference sheets
|
| 155 |
+
- session manager UI
|
| 156 |
+
- command palette
|
| 157 |
+
- permissions/cert/offline/print/DoH
|
| 158 |
+
|
| 159 |
+
## Phase 5 target
|
| 160 |
+
|
| 161 |
+
Windows v1 shipping:
|
| 162 |
+
- NSIS installer
|
| 163 |
+
- updater
|
| 164 |
+
- code signing
|
| 165 |
+
- optional crash reporting
|
| 166 |
+
- performance audit
|
| 167 |
+
|
| 168 |
+
## Current implementation note
|
| 169 |
+
|
| 170 |
+
The committed code implements the Phase 0 scope only. Phase 1+ modules are represented as directory placeholders and comments where useful, but no tab WebViews are created in Phase 0.
|
index.html
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>Refstudio</title>
|
| 7 |
-
</head>
|
| 8 |
-
<body>
|
| 9 |
-
<div id="root"></div>
|
| 10 |
-
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
-
</body>
|
| 12 |
-
</html>
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Refstudio</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "refstudio",
|
| 3 |
-
"private": true,
|
| 4 |
-
"version": "1.0.0-alpha",
|
| 5 |
-
"type": "module",
|
| 6 |
-
"scripts": {
|
| 7 |
-
"dev": "vite --port 1420 --host",
|
| 8 |
-
"build": "vite build",
|
| 9 |
-
"preview": "vite preview",
|
| 10 |
-
"tauri": "tauri"
|
| 11 |
-
},
|
| 12 |
-
"dependencies": {
|
| 13 |
-
"@tauri-apps/api": "^2
|
| 14 |
-
"@tauri-apps/plugin-clipboard-manager": "^2
|
| 15 |
-
"@tauri-apps/plugin-fs": "^2
|
| 16 |
-
"@tauri-apps/plugin-opener": "^2
|
| 17 |
-
"@tauri-apps/plugin-store": "^2
|
| 18 |
-
"@tauri-apps/plugin-stronghold": "^2
|
| 19 |
-
"lucide-react": "^0.
|
| 20 |
-
"motion": "^12.
|
| 21 |
-
"react": "^19.0.0",
|
| 22 |
-
"react-dom": "^19.0.0"
|
| 23 |
-
},
|
| 24 |
-
"devDependencies": {
|
| 25 |
-
"@tauri-apps/cli": "^2
|
| 26 |
-
"@tailwindcss/vite": "^4.
|
| 27 |
-
"@types/react": "^19.0.0",
|
| 28 |
-
"@types/react-dom": "^19.0.0",
|
| 29 |
-
"@vitejs/plugin-react": "^
|
| 30 |
-
"autoprefixer": "^10.4.
|
| 31 |
-
"postcss": "^8.
|
| 32 |
-
"tailwindcss": "^4.
|
| 33 |
-
"typescript": "~5.
|
| 34 |
-
"vite": "^6.
|
| 35 |
-
}
|
| 36 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "refstudio",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0-alpha",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite --port 1420 --host",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview",
|
| 10 |
+
"tauri": "tauri"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@tauri-apps/api": "^2",
|
| 14 |
+
"@tauri-apps/plugin-clipboard-manager": "^2",
|
| 15 |
+
"@tauri-apps/plugin-fs": "^2",
|
| 16 |
+
"@tauri-apps/plugin-opener": "^2",
|
| 17 |
+
"@tauri-apps/plugin-store": "^2",
|
| 18 |
+
"@tauri-apps/plugin-stronghold": "^2",
|
| 19 |
+
"lucide-react": "^0.468.0",
|
| 20 |
+
"motion": "^12.0.0",
|
| 21 |
+
"react": "^19.0.0",
|
| 22 |
+
"react-dom": "^19.0.0"
|
| 23 |
+
},
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"@tauri-apps/cli": "^2",
|
| 26 |
+
"@tailwindcss/vite": "^4.0.0",
|
| 27 |
+
"@types/react": "^19.0.0",
|
| 28 |
+
"@types/react-dom": "^19.0.0",
|
| 29 |
+
"@vitejs/plugin-react": "^4.3.0",
|
| 30 |
+
"autoprefixer": "^10.4.20",
|
| 31 |
+
"postcss": "^8.4.49",
|
| 32 |
+
"tailwindcss": "^4.0.0",
|
| 33 |
+
"typescript": "~5.6.2",
|
| 34 |
+
"vite": "^6.0.0"
|
| 35 |
+
}
|
| 36 |
+
}
|
src-tauri/Cargo.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src-tauri/Cargo.toml
CHANGED
|
@@ -39,6 +39,8 @@ base64 = "0.22"
|
|
| 39 |
uuid = { version = "1", features = ["v4", "serde"] }
|
| 40 |
chrono = { version = "0.4", features = ["serde"] }
|
| 41 |
screenshots = "0.8"
|
|
|
|
|
|
|
| 42 |
|
| 43 |
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
| 44 |
tauri-plugin-global-shortcut = "2"
|
|
|
|
| 39 |
uuid = { version = "1", features = ["v4", "serde"] }
|
| 40 |
chrono = { version = "0.4", features = ["serde"] }
|
| 41 |
screenshots = "0.8"
|
| 42 |
+
sha2 = "0.10"
|
| 43 |
+
zip = { version = "2.4", default-features = false, features = ["deflate"] }
|
| 44 |
|
| 45 |
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
| 46 |
tauri-plugin-global-shortcut = "2"
|
src-tauri/build.rs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
fn main() {
|
| 2 |
-
tauri_build::build()
|
| 3 |
-
}
|
|
|
|
| 1 |
+
fn main() {
|
| 2 |
+
tauri_build::build()
|
| 3 |
+
}
|
src-tauri/capabilities/default.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
| 1 |
-
{
|
| 2 |
-
"$schema": "../gen/schemas/desktop-schema.json",
|
| 3 |
-
"identifier": "default",
|
| 4 |
-
"description": "MUSE Alpha Phase 3 production capabilities",
|
| 5 |
-
"windows": ["main"],
|
| 6 |
-
"remote": {
|
| 7 |
-
"urls": ["https://*"]
|
| 8 |
-
},
|
| 9 |
-
"permissions": [
|
| 10 |
-
"core:default",
|
| 11 |
-
"opener:default",
|
| 12 |
-
"core:window:allow-start-dragging",
|
| 13 |
-
"core:window:allow-minimize",
|
| 14 |
-
"core:window:allow-toggle-maximize",
|
| 15 |
-
"core:window:allow-close",
|
| 16 |
-
"core:window:allow-inner-size",
|
| 17 |
-
"store:default",
|
| 18 |
-
"sql:default",
|
| 19 |
-
"sql:allow-execute",
|
| 20 |
-
"fs:default",
|
| 21 |
-
"clipboard-manager:allow-read-text",
|
| 22 |
-
"clipboard-manager:allow-write-text",
|
| 23 |
-
"global-shortcut:allow-register",
|
| 24 |
-
"global-shortcut:allow-unregister",
|
| 25 |
-
"global-shortcut:allow-unregister-all",
|
| 26 |
-
"global-shortcut:allow-is-registered",
|
| 27 |
-
"stronghold:default"
|
| 28 |
-
]
|
| 29 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "../gen/schemas/desktop-schema.json",
|
| 3 |
+
"identifier": "default",
|
| 4 |
+
"description": "MUSE Alpha Phase 3 production capabilities",
|
| 5 |
+
"windows": ["main"],
|
| 6 |
+
"remote": {
|
| 7 |
+
"urls": ["https://*"]
|
| 8 |
+
},
|
| 9 |
+
"permissions": [
|
| 10 |
+
"core:default",
|
| 11 |
+
"opener:default",
|
| 12 |
+
"core:window:allow-start-dragging",
|
| 13 |
+
"core:window:allow-minimize",
|
| 14 |
+
"core:window:allow-toggle-maximize",
|
| 15 |
+
"core:window:allow-close",
|
| 16 |
+
"core:window:allow-inner-size",
|
| 17 |
+
"store:default",
|
| 18 |
+
"sql:default",
|
| 19 |
+
"sql:allow-execute",
|
| 20 |
+
"fs:default",
|
| 21 |
+
"clipboard-manager:allow-read-text",
|
| 22 |
+
"clipboard-manager:allow-write-text",
|
| 23 |
+
"global-shortcut:allow-register",
|
| 24 |
+
"global-shortcut:allow-unregister",
|
| 25 |
+
"global-shortcut:allow-unregister-all",
|
| 26 |
+
"global-shortcut:allow-is-registered",
|
| 27 |
+
"stronghold:default"
|
| 28 |
+
]
|
| 29 |
+
}
|
src-tauri/gen/schemas/acl-manifests.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src-tauri/gen/schemas/capabilities.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"default":{"identifier":"default","description":"MUSE Alpha Phase 3 production capabilities","remote":{"urls":["https://*"]},"local":true,"windows":["main"],"permissions":["core:default","opener:default","core:window:allow-start-dragging","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-close","core:window:allow-inner-size","store:default","sql:default","sql:allow-execute","fs:default","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","global-shortcut:allow-register","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","global-shortcut:allow-is-registered","stronghold:default"]}}
|
src-tauri/gen/schemas/desktop-schema.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src-tauri/gen/schemas/windows-schema.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src-tauri/icons/README.md
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
Add generated Tauri icon assets here before packaging (`pnpm tauri icon`).
|
|
|
|
| 1 |
+
Add generated Tauri icon assets here before packaging (`pnpm tauri icon`).
|
src-tauri/migrations/001_phase0_init.sql
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
-
CREATE TABLE IF NOT EXISTS app_meta (
|
| 2 |
-
key TEXT PRIMARY KEY NOT NULL,
|
| 3 |
-
value TEXT NOT NULL,
|
| 4 |
-
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 5 |
-
);
|
| 6 |
-
|
| 7 |
-
CREATE TABLE IF NOT EXISTS settings (
|
| 8 |
-
key TEXT PRIMARY KEY NOT NULL,
|
| 9 |
-
value_json TEXT NOT NULL,
|
| 10 |
-
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 11 |
-
);
|
| 12 |
-
|
| 13 |
-
CREATE TABLE IF NOT EXISTS workspaces (
|
| 14 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 15 |
-
name TEXT NOT NULL,
|
| 16 |
-
accent TEXT NOT NULL DEFAULT '#C49A3C',
|
| 17 |
-
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 18 |
-
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 19 |
-
);
|
| 20 |
-
|
| 21 |
-
INSERT OR IGNORE INTO app_meta (key, value) VALUES ('schema_version', '1');
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS app_meta (
|
| 2 |
+
key TEXT PRIMARY KEY NOT NULL,
|
| 3 |
+
value TEXT NOT NULL,
|
| 4 |
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 5 |
+
);
|
| 6 |
+
|
| 7 |
+
CREATE TABLE IF NOT EXISTS settings (
|
| 8 |
+
key TEXT PRIMARY KEY NOT NULL,
|
| 9 |
+
value_json TEXT NOT NULL,
|
| 10 |
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 11 |
+
);
|
| 12 |
+
|
| 13 |
+
CREATE TABLE IF NOT EXISTS workspaces (
|
| 14 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 15 |
+
name TEXT NOT NULL,
|
| 16 |
+
accent TEXT NOT NULL DEFAULT '#C49A3C',
|
| 17 |
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 18 |
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
INSERT OR IGNORE INTO app_meta (key, value) VALUES ('schema_version', '1');
|
src-tauri/migrations/002_phase3_tables.sql
CHANGED
|
@@ -1,94 +1,94 @@
|
|
| 1 |
-
-- Library items table
|
| 2 |
-
CREATE TABLE IF NOT EXISTS library_items (
|
| 3 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 4 |
-
url TEXT NOT NULL,
|
| 5 |
-
source_url TEXT NOT NULL DEFAULT '',
|
| 6 |
-
title TEXT NOT NULL DEFAULT 'Untitled',
|
| 7 |
-
hash TEXT NOT NULL,
|
| 8 |
-
width INTEGER NOT NULL DEFAULT 0,
|
| 9 |
-
height INTEGER NOT NULL DEFAULT 0,
|
| 10 |
-
colors TEXT NOT NULL DEFAULT '[]',
|
| 11 |
-
tags TEXT NOT NULL DEFAULT '[]',
|
| 12 |
-
notes TEXT NOT NULL DEFAULT '',
|
| 13 |
-
created_at INTEGER NOT NULL DEFAULT 0
|
| 14 |
-
);
|
| 15 |
-
|
| 16 |
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_library_hash ON library_items(hash);
|
| 17 |
-
|
| 18 |
-
-- FTS5 for library search
|
| 19 |
-
CREATE VIRTUAL TABLE IF NOT EXISTS library_fts USING fts5(
|
| 20 |
-
title, tags, notes, source_url,
|
| 21 |
-
content='library_items',
|
| 22 |
-
content_rowid='rowid'
|
| 23 |
-
);
|
| 24 |
-
|
| 25 |
-
-- Board items table
|
| 26 |
-
CREATE TABLE IF NOT EXISTS board_items (
|
| 27 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 28 |
-
kind TEXT NOT NULL DEFAULT 'image',
|
| 29 |
-
library_id TEXT,
|
| 30 |
-
text TEXT,
|
| 31 |
-
colors TEXT NOT NULL DEFAULT '[]',
|
| 32 |
-
x REAL NOT NULL DEFAULT 0,
|
| 33 |
-
y REAL NOT NULL DEFAULT 0,
|
| 34 |
-
w REAL NOT NULL DEFAULT 200,
|
| 35 |
-
h REAL NOT NULL DEFAULT 200,
|
| 36 |
-
z INTEGER NOT NULL DEFAULT 0
|
| 37 |
-
);
|
| 38 |
-
|
| 39 |
-
-- Sessions
|
| 40 |
-
CREATE TABLE IF NOT EXISTS sessions (
|
| 41 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 42 |
-
name TEXT NOT NULL,
|
| 43 |
-
tabs TEXT NOT NULL DEFAULT '[]',
|
| 44 |
-
created_at INTEGER NOT NULL DEFAULT 0
|
| 45 |
-
);
|
| 46 |
-
|
| 47 |
-
-- Downloads
|
| 48 |
-
CREATE TABLE IF NOT EXISTS downloads (
|
| 49 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 50 |
-
url TEXT NOT NULL,
|
| 51 |
-
filename TEXT NOT NULL DEFAULT '',
|
| 52 |
-
status TEXT NOT NULL DEFAULT 'pending',
|
| 53 |
-
saved_to_library INTEGER NOT NULL DEFAULT 0,
|
| 54 |
-
created_at INTEGER NOT NULL DEFAULT 0
|
| 55 |
-
);
|
| 56 |
-
|
| 57 |
-
-- Credentials index (passwords stored in Stronghold, only metadata here)
|
| 58 |
-
CREATE TABLE IF NOT EXISTS credentials (
|
| 59 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 60 |
-
domain TEXT NOT NULL,
|
| 61 |
-
username TEXT NOT NULL,
|
| 62 |
-
vault_key TEXT NOT NULL,
|
| 63 |
-
label TEXT NOT NULL DEFAULT '',
|
| 64 |
-
last_used INTEGER NOT NULL DEFAULT 0,
|
| 65 |
-
created_at INTEGER NOT NULL DEFAULT 0
|
| 66 |
-
);
|
| 67 |
-
|
| 68 |
-
CREATE INDEX IF NOT EXISTS idx_credentials_domain ON credentials(domain);
|
| 69 |
-
|
| 70 |
-
-- Adblock user rules
|
| 71 |
-
CREATE TABLE IF NOT EXISTS adblock_user_rules (
|
| 72 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 73 |
-
rule_text TEXT NOT NULL,
|
| 74 |
-
enabled INTEGER NOT NULL DEFAULT 1,
|
| 75 |
-
comment TEXT NOT NULL DEFAULT '',
|
| 76 |
-
created_at INTEGER NOT NULL DEFAULT 0
|
| 77 |
-
);
|
| 78 |
-
|
| 79 |
-
-- Adblock subscriptions
|
| 80 |
-
CREATE TABLE IF NOT EXISTS adblock_subscriptions (
|
| 81 |
-
id TEXT PRIMARY KEY NOT NULL,
|
| 82 |
-
name TEXT NOT NULL,
|
| 83 |
-
url TEXT NOT NULL UNIQUE,
|
| 84 |
-
enabled INTEGER NOT NULL DEFAULT 1,
|
| 85 |
-
last_updated INTEGER NOT NULL DEFAULT 0,
|
| 86 |
-
is_builtin INTEGER NOT NULL DEFAULT 0
|
| 87 |
-
);
|
| 88 |
-
|
| 89 |
-
INSERT OR IGNORE INTO adblock_subscriptions (id, name, url, enabled, is_builtin) VALUES
|
| 90 |
-
('easylist', 'EasyList', 'https://easylist.to/easylist/easylist.txt', 1, 1),
|
| 91 |
-
('easyprivacy', 'EasyPrivacy', 'https://easylist.to/easylist/easyprivacy.txt', 1, 1),
|
| 92 |
-
('fanboy', 'Fanboy Annoyances', 'https://secure.fanboy.co.nz/fanboy-annoyance.txt', 1, 1),
|
| 93 |
-
('ublock', 'uBlock Filters', 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt', 1, 1),
|
| 94 |
-
('peter-lowe', 'Peter Lowe', 'https://pgl.yoyo.org/adservers/serverlist.php?mimetype=plaintext', 1, 1);
|
|
|
|
| 1 |
+
-- Library items table
|
| 2 |
+
CREATE TABLE IF NOT EXISTS library_items (
|
| 3 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 4 |
+
url TEXT NOT NULL,
|
| 5 |
+
source_url TEXT NOT NULL DEFAULT '',
|
| 6 |
+
title TEXT NOT NULL DEFAULT 'Untitled',
|
| 7 |
+
hash TEXT NOT NULL,
|
| 8 |
+
width INTEGER NOT NULL DEFAULT 0,
|
| 9 |
+
height INTEGER NOT NULL DEFAULT 0,
|
| 10 |
+
colors TEXT NOT NULL DEFAULT '[]',
|
| 11 |
+
tags TEXT NOT NULL DEFAULT '[]',
|
| 12 |
+
notes TEXT NOT NULL DEFAULT '',
|
| 13 |
+
created_at INTEGER NOT NULL DEFAULT 0
|
| 14 |
+
);
|
| 15 |
+
|
| 16 |
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_library_hash ON library_items(hash);
|
| 17 |
+
|
| 18 |
+
-- FTS5 for library search
|
| 19 |
+
CREATE VIRTUAL TABLE IF NOT EXISTS library_fts USING fts5(
|
| 20 |
+
title, tags, notes, source_url,
|
| 21 |
+
content='library_items',
|
| 22 |
+
content_rowid='rowid'
|
| 23 |
+
);
|
| 24 |
+
|
| 25 |
+
-- Board items table
|
| 26 |
+
CREATE TABLE IF NOT EXISTS board_items (
|
| 27 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 28 |
+
kind TEXT NOT NULL DEFAULT 'image',
|
| 29 |
+
library_id TEXT,
|
| 30 |
+
text TEXT,
|
| 31 |
+
colors TEXT NOT NULL DEFAULT '[]',
|
| 32 |
+
x REAL NOT NULL DEFAULT 0,
|
| 33 |
+
y REAL NOT NULL DEFAULT 0,
|
| 34 |
+
w REAL NOT NULL DEFAULT 200,
|
| 35 |
+
h REAL NOT NULL DEFAULT 200,
|
| 36 |
+
z INTEGER NOT NULL DEFAULT 0
|
| 37 |
+
);
|
| 38 |
+
|
| 39 |
+
-- Sessions
|
| 40 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 41 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 42 |
+
name TEXT NOT NULL,
|
| 43 |
+
tabs TEXT NOT NULL DEFAULT '[]',
|
| 44 |
+
created_at INTEGER NOT NULL DEFAULT 0
|
| 45 |
+
);
|
| 46 |
+
|
| 47 |
+
-- Downloads
|
| 48 |
+
CREATE TABLE IF NOT EXISTS downloads (
|
| 49 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 50 |
+
url TEXT NOT NULL,
|
| 51 |
+
filename TEXT NOT NULL DEFAULT '',
|
| 52 |
+
status TEXT NOT NULL DEFAULT 'pending',
|
| 53 |
+
saved_to_library INTEGER NOT NULL DEFAULT 0,
|
| 54 |
+
created_at INTEGER NOT NULL DEFAULT 0
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
-- Credentials index (passwords stored in Stronghold, only metadata here)
|
| 58 |
+
CREATE TABLE IF NOT EXISTS credentials (
|
| 59 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 60 |
+
domain TEXT NOT NULL,
|
| 61 |
+
username TEXT NOT NULL,
|
| 62 |
+
vault_key TEXT NOT NULL,
|
| 63 |
+
label TEXT NOT NULL DEFAULT '',
|
| 64 |
+
last_used INTEGER NOT NULL DEFAULT 0,
|
| 65 |
+
created_at INTEGER NOT NULL DEFAULT 0
|
| 66 |
+
);
|
| 67 |
+
|
| 68 |
+
CREATE INDEX IF NOT EXISTS idx_credentials_domain ON credentials(domain);
|
| 69 |
+
|
| 70 |
+
-- Adblock user rules
|
| 71 |
+
CREATE TABLE IF NOT EXISTS adblock_user_rules (
|
| 72 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 73 |
+
rule_text TEXT NOT NULL,
|
| 74 |
+
enabled INTEGER NOT NULL DEFAULT 1,
|
| 75 |
+
comment TEXT NOT NULL DEFAULT '',
|
| 76 |
+
created_at INTEGER NOT NULL DEFAULT 0
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
-- Adblock subscriptions
|
| 80 |
+
CREATE TABLE IF NOT EXISTS adblock_subscriptions (
|
| 81 |
+
id TEXT PRIMARY KEY NOT NULL,
|
| 82 |
+
name TEXT NOT NULL,
|
| 83 |
+
url TEXT NOT NULL UNIQUE,
|
| 84 |
+
enabled INTEGER NOT NULL DEFAULT 1,
|
| 85 |
+
last_updated INTEGER NOT NULL DEFAULT 0,
|
| 86 |
+
is_builtin INTEGER NOT NULL DEFAULT 0
|
| 87 |
+
);
|
| 88 |
+
|
| 89 |
+
INSERT OR IGNORE INTO adblock_subscriptions (id, name, url, enabled, is_builtin) VALUES
|
| 90 |
+
('easylist', 'EasyList', 'https://easylist.to/easylist/easylist.txt', 1, 1),
|
| 91 |
+
('easyprivacy', 'EasyPrivacy', 'https://easylist.to/easylist/easyprivacy.txt', 1, 1),
|
| 92 |
+
('fanboy', 'Fanboy Annoyances', 'https://secure.fanboy.co.nz/fanboy-annoyance.txt', 1, 1),
|
| 93 |
+
('ublock', 'uBlock Filters', 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt', 1, 1),
|
| 94 |
+
('peter-lowe', 'Peter Lowe', 'https://pgl.yoyo.org/adservers/serverlist.php?mimetype=plaintext', 1, 1);
|
src-tauri/resources/filters/annoyances_mini.txt
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
-
! Title: Muse Annoyances Mini (bundled subset)
|
| 2 |
-
! Cookie consent auto-dismiss
|
| 3 |
-
||onetrust.com^$third-party
|
| 4 |
-
||cookielaw.org^$third-party
|
| 5 |
-
||trustarc.com^$third-party
|
| 6 |
-
||consensu.org^$third-party
|
| 7 |
-
||cookiebot.com^$third-party
|
| 8 |
-
||cookiepro.com^$third-party
|
| 9 |
-
||iubenda.com^$third-party
|
| 10 |
-
||termly.io^$third-party
|
| 11 |
-
! Newsletter/signup popups
|
| 12 |
-
||sumo.com^$third-party
|
| 13 |
-
||privy.com^$third-party
|
| 14 |
-
||optinmonster.com^$third-party
|
| 15 |
-
||mailchimp.com^$third-party,image
|
| 16 |
-
||convertkit.com^$third-party
|
| 17 |
-
! Social widgets
|
| 18 |
-
||platform.twitter.com/widgets^$third-party
|
| 19 |
-
||connect.facebook.net/*/sdk.js^$third-party
|
| 20 |
-
||apis.google.com/js/plusone^$third-party
|
|
|
|
| 1 |
+
! Title: Muse Annoyances Mini (bundled subset)
|
| 2 |
+
! Cookie consent auto-dismiss
|
| 3 |
+
||onetrust.com^$third-party
|
| 4 |
+
||cookielaw.org^$third-party
|
| 5 |
+
||trustarc.com^$third-party
|
| 6 |
+
||consensu.org^$third-party
|
| 7 |
+
||cookiebot.com^$third-party
|
| 8 |
+
||cookiepro.com^$third-party
|
| 9 |
+
||iubenda.com^$third-party
|
| 10 |
+
||termly.io^$third-party
|
| 11 |
+
! Newsletter/signup popups
|
| 12 |
+
||sumo.com^$third-party
|
| 13 |
+
||privy.com^$third-party
|
| 14 |
+
||optinmonster.com^$third-party
|
| 15 |
+
||mailchimp.com^$third-party,image
|
| 16 |
+
||convertkit.com^$third-party
|
| 17 |
+
! Social widgets
|
| 18 |
+
||platform.twitter.com/widgets^$third-party
|
| 19 |
+
||connect.facebook.net/*/sdk.js^$third-party
|
| 20 |
+
||apis.google.com/js/plusone^$third-party
|
src-tauri/resources/filters/easylist_mini.txt
CHANGED
|
@@ -1,51 +1,51 @@
|
|
| 1 |
-
! Title: Muse EasyList Mini (bundled subset)
|
| 2 |
-
! Compact version for instant startup. Full lists loaded via daily updater.
|
| 3 |
-
||googlesyndication.com^
|
| 4 |
-
||doubleclick.net^
|
| 5 |
-
||google-analytics.com^
|
| 6 |
-
||googletagmanager.com/gtag^$third-party
|
| 7 |
-
||amazon-adsystem.com^
|
| 8 |
-
||facebook.net/tr^
|
| 9 |
-
||connect.facebook.net^$third-party
|
| 10 |
-
||adnxs.com^
|
| 11 |
-
||criteo.com^
|
| 12 |
-
||outbrain.com^
|
| 13 |
-
||taboola.com^
|
| 14 |
-
||rubiconproject.com^
|
| 15 |
-
||pubmatic.com^
|
| 16 |
-
||openx.net^
|
| 17 |
-
||moatads.com^
|
| 18 |
-
||2mdn.net^
|
| 19 |
-
||adsymptotic.com^
|
| 20 |
-
||advertising.com^
|
| 21 |
-
||scorecardresearch.com^
|
| 22 |
-
||quantserve.com^
|
| 23 |
-
||bluekai.com^
|
| 24 |
-
||turn.com^
|
| 25 |
-
||serving-sys.com^
|
| 26 |
-
||bidswitch.net^
|
| 27 |
-
||media.net^
|
| 28 |
-
||casalemedia.com^
|
| 29 |
-
||indexexchange.com^
|
| 30 |
-
||mathtag.com^
|
| 31 |
-
||adform.net^
|
| 32 |
-
||eyeota.net^
|
| 33 |
-
||sharethrough.com^
|
| 34 |
-
||smartadserver.com^
|
| 35 |
-
||rlcdn.com^
|
| 36 |
-
||demdex.net^
|
| 37 |
-
||agkn.com^
|
| 38 |
-
||3lift.com^
|
| 39 |
-
||33across.com^
|
| 40 |
-
||liadm.com^
|
| 41 |
-
||adsrvr.org^
|
| 42 |
-
||contextweb.com^
|
| 43 |
-
||everesttech.net^
|
| 44 |
-
||krxd.net^
|
| 45 |
-
||yieldmo.com^
|
| 46 |
-
! Popular site-specific
|
| 47 |
-
||pagead2.googlesyndication.com^
|
| 48 |
-
||tpc.googlesyndication.com^
|
| 49 |
-
||securepubads.g.doubleclick.net^
|
| 50 |
-
||ad.doubleclick.net^
|
| 51 |
-
||stats.g.doubleclick.net^
|
|
|
|
| 1 |
+
! Title: Muse EasyList Mini (bundled subset)
|
| 2 |
+
! Compact version for instant startup. Full lists loaded via daily updater.
|
| 3 |
+
||googlesyndication.com^
|
| 4 |
+
||doubleclick.net^
|
| 5 |
+
||google-analytics.com^
|
| 6 |
+
||googletagmanager.com/gtag^$third-party
|
| 7 |
+
||amazon-adsystem.com^
|
| 8 |
+
||facebook.net/tr^
|
| 9 |
+
||connect.facebook.net^$third-party
|
| 10 |
+
||adnxs.com^
|
| 11 |
+
||criteo.com^
|
| 12 |
+
||outbrain.com^
|
| 13 |
+
||taboola.com^
|
| 14 |
+
||rubiconproject.com^
|
| 15 |
+
||pubmatic.com^
|
| 16 |
+
||openx.net^
|
| 17 |
+
||moatads.com^
|
| 18 |
+
||2mdn.net^
|
| 19 |
+
||adsymptotic.com^
|
| 20 |
+
||advertising.com^
|
| 21 |
+
||scorecardresearch.com^
|
| 22 |
+
||quantserve.com^
|
| 23 |
+
||bluekai.com^
|
| 24 |
+
||turn.com^
|
| 25 |
+
||serving-sys.com^
|
| 26 |
+
||bidswitch.net^
|
| 27 |
+
||media.net^
|
| 28 |
+
||casalemedia.com^
|
| 29 |
+
||indexexchange.com^
|
| 30 |
+
||mathtag.com^
|
| 31 |
+
||adform.net^
|
| 32 |
+
||eyeota.net^
|
| 33 |
+
||sharethrough.com^
|
| 34 |
+
||smartadserver.com^
|
| 35 |
+
||rlcdn.com^
|
| 36 |
+
||demdex.net^
|
| 37 |
+
||agkn.com^
|
| 38 |
+
||3lift.com^
|
| 39 |
+
||33across.com^
|
| 40 |
+
||liadm.com^
|
| 41 |
+
||adsrvr.org^
|
| 42 |
+
||contextweb.com^
|
| 43 |
+
||everesttech.net^
|
| 44 |
+
||krxd.net^
|
| 45 |
+
||yieldmo.com^
|
| 46 |
+
! Popular site-specific
|
| 47 |
+
||pagead2.googlesyndication.com^
|
| 48 |
+
||tpc.googlesyndication.com^
|
| 49 |
+
||securepubads.g.doubleclick.net^
|
| 50 |
+
||ad.doubleclick.net^
|
| 51 |
+
||stats.g.doubleclick.net^
|
src-tauri/resources/filters/easyprivacy_mini.txt
CHANGED
|
@@ -1,28 +1,28 @@
|
|
| 1 |
-
! Title: Muse EasyPrivacy Mini (bundled subset)
|
| 2 |
-
||hotjar.com^
|
| 3 |
-
||mixpanel.com^
|
| 4 |
-
||segment.io^
|
| 5 |
-
||segment.com^
|
| 6 |
-
||amplitude.com^
|
| 7 |
-
||fullstory.com^
|
| 8 |
-
||heap-analytics.com^
|
| 9 |
-
||crazyegg.com^
|
| 10 |
-
||mouseflow.com^
|
| 11 |
-
||clarity.ms^
|
| 12 |
-
||newrelic.com^
|
| 13 |
-
||nr-data.net^
|
| 14 |
-
||sentry.io^$third-party
|
| 15 |
-
||bugsnag.com^$third-party
|
| 16 |
-
||rollbar.com^$third-party
|
| 17 |
-
||optimizely.com^$third-party
|
| 18 |
-
||branch.io^$third-party
|
| 19 |
-
||appsflyer.com^
|
| 20 |
-
||adjust.com^$third-party
|
| 21 |
-
||app.link^$third-party
|
| 22 |
-
||tealiumiq.com^
|
| 23 |
-
||omtrdc.net^
|
| 24 |
-
||2o7.net^
|
| 25 |
-
||hit.gemius.pl^
|
| 26 |
-
||chartbeat.com^$third-party
|
| 27 |
-
||parsely.com^$third-party
|
| 28 |
-
||pixel.wp.com^$third-party
|
|
|
|
| 1 |
+
! Title: Muse EasyPrivacy Mini (bundled subset)
|
| 2 |
+
||hotjar.com^
|
| 3 |
+
||mixpanel.com^
|
| 4 |
+
||segment.io^
|
| 5 |
+
||segment.com^
|
| 6 |
+
||amplitude.com^
|
| 7 |
+
||fullstory.com^
|
| 8 |
+
||heap-analytics.com^
|
| 9 |
+
||crazyegg.com^
|
| 10 |
+
||mouseflow.com^
|
| 11 |
+
||clarity.ms^
|
| 12 |
+
||newrelic.com^
|
| 13 |
+
||nr-data.net^
|
| 14 |
+
||sentry.io^$third-party
|
| 15 |
+
||bugsnag.com^$third-party
|
| 16 |
+
||rollbar.com^$third-party
|
| 17 |
+
||optimizely.com^$third-party
|
| 18 |
+
||branch.io^$third-party
|
| 19 |
+
||appsflyer.com^
|
| 20 |
+
||adjust.com^$third-party
|
| 21 |
+
||app.link^$third-party
|
| 22 |
+
||tealiumiq.com^
|
| 23 |
+
||omtrdc.net^
|
| 24 |
+
||2o7.net^
|
| 25 |
+
||hit.gemius.pl^
|
| 26 |
+
||chartbeat.com^$third-party
|
| 27 |
+
||parsely.com^$third-party
|
| 28 |
+
||pixel.wp.com^$third-party
|
src-tauri/resources/scriptlets/muse_ubo_compatible_scriptlets.js
CHANGED
|
@@ -1,105 +1,105 @@
|
|
| 1 |
-
/// json-prune.js
|
| 2 |
-
(function() {
|
| 3 |
-
'use strict';
|
| 4 |
-
const args = Array.from(arguments);
|
| 5 |
-
const prunePaths = (args[0] || '').split(/\s+/).filter(Boolean);
|
| 6 |
-
if (prunePaths.length === 0) return;
|
| 7 |
-
function prune(obj, path) {
|
| 8 |
-
if (!obj || typeof obj !== 'object') return;
|
| 9 |
-
const parts = path.split('.');
|
| 10 |
-
let target = obj;
|
| 11 |
-
for (let i = 0; i < parts.length - 1; i++) {
|
| 12 |
-
const part = parts[i];
|
| 13 |
-
if (part.endsWith('[]')) {
|
| 14 |
-
const key = part.slice(0, -2);
|
| 15 |
-
if (Array.isArray(target[key])) {
|
| 16 |
-
target[key].forEach(item => prune(item, parts.slice(i + 1).join('.')));
|
| 17 |
-
}
|
| 18 |
-
return;
|
| 19 |
-
}
|
| 20 |
-
if (!target[part]) return;
|
| 21 |
-
target = target[part];
|
| 22 |
-
}
|
| 23 |
-
delete target[parts[parts.length - 1]];
|
| 24 |
-
}
|
| 25 |
-
function pruneAll(obj) { for (const p of prunePaths) prune(obj, p); return obj; }
|
| 26 |
-
const jp = JSON.parse;
|
| 27 |
-
JSON.parse = function() { return pruneAll(jp.apply(this, arguments)); };
|
| 28 |
-
const rj = Response.prototype.json;
|
| 29 |
-
Response.prototype.json = function() { return rj.apply(this, arguments).then(pruneAll); };
|
| 30 |
-
})();
|
| 31 |
-
|
| 32 |
-
/// set-constant.js
|
| 33 |
-
(function() {
|
| 34 |
-
'use strict';
|
| 35 |
-
const prop = arguments[0];
|
| 36 |
-
const raw = arguments[1];
|
| 37 |
-
if (!prop) return;
|
| 38 |
-
let value;
|
| 39 |
-
if (raw === 'undefined') value = undefined;
|
| 40 |
-
else if (raw === 'false') value = false;
|
| 41 |
-
else if (raw === 'true') value = true;
|
| 42 |
-
else if (raw === 'null') value = null;
|
| 43 |
-
else value = raw;
|
| 44 |
-
const parts = prop.split('.');
|
| 45 |
-
let obj = window;
|
| 46 |
-
for (let i = 0; i < parts.length - 1; i++) {
|
| 47 |
-
obj = obj[parts[i]] = obj[parts[i]] || {};
|
| 48 |
-
}
|
| 49 |
-
try {
|
| 50 |
-
Object.defineProperty(obj, parts[parts.length - 1], { get: () => value, set: () => {}, configurable: true });
|
| 51 |
-
} catch {}
|
| 52 |
-
})();
|
| 53 |
-
|
| 54 |
-
/// abort-current-script.js
|
| 55 |
-
(function() {
|
| 56 |
-
'use strict';
|
| 57 |
-
const needle = arguments[0] || '';
|
| 58 |
-
const prop = arguments[1] || '';
|
| 59 |
-
if (!needle && !prop) return;
|
| 60 |
-
const re = new RegExp(needle);
|
| 61 |
-
const oe = document.createElement.bind(document);
|
| 62 |
-
document.createElement = function(tag) {
|
| 63 |
-
const el = oe(tag);
|
| 64 |
-
if (String(tag).toLowerCase() === 'script') {
|
| 65 |
-
const set = el.setAttribute.bind(el);
|
| 66 |
-
el.setAttribute = function(name, value) {
|
| 67 |
-
if (name === 'src' && re.test(String(value))) return;
|
| 68 |
-
return set(name, value);
|
| 69 |
-
};
|
| 70 |
-
}
|
| 71 |
-
return el;
|
| 72 |
-
};
|
| 73 |
-
})();
|
| 74 |
-
|
| 75 |
-
/// prevent-fetch.js
|
| 76 |
-
(function() {
|
| 77 |
-
'use strict';
|
| 78 |
-
const needle = arguments[0] || '';
|
| 79 |
-
if (!needle) return;
|
| 80 |
-
const re = new RegExp(needle);
|
| 81 |
-
const of = window.fetch;
|
| 82 |
-
window.fetch = function(input, init) {
|
| 83 |
-
const url = typeof input === 'string' ? input : input && input.url || '';
|
| 84 |
-
if (re.test(url)) return Promise.resolve(new Response('', { status: 204 }));
|
| 85 |
-
return of.apply(this, arguments);
|
| 86 |
-
};
|
| 87 |
-
})();
|
| 88 |
-
|
| 89 |
-
/// m3u8-prune.js
|
| 90 |
-
(function() {
|
| 91 |
-
'use strict';
|
| 92 |
-
const needle = arguments[0] || 'stitched-ad|advertisement|DATERANGE';
|
| 93 |
-
const re = new RegExp(needle);
|
| 94 |
-
const of = window.fetch;
|
| 95 |
-
window.fetch = function(input, init) {
|
| 96 |
-
const url = typeof input === 'string' ? input : input && input.url || '';
|
| 97 |
-
return of.apply(this, arguments).then(resp => {
|
| 98 |
-
if (!String(url).includes('.m3u8')) return resp;
|
| 99 |
-
return resp.clone().text().then(text => {
|
| 100 |
-
const out = text.split('\n').filter(line => !re.test(line)).join('\n');
|
| 101 |
-
return new Response(out, { status: resp.status, headers: resp.headers });
|
| 102 |
-
}).catch(() => resp);
|
| 103 |
-
});
|
| 104 |
-
};
|
| 105 |
-
})();
|
|
|
|
| 1 |
+
/// json-prune.js
|
| 2 |
+
(function() {
|
| 3 |
+
'use strict';
|
| 4 |
+
const args = Array.from(arguments);
|
| 5 |
+
const prunePaths = (args[0] || '').split(/\s+/).filter(Boolean);
|
| 6 |
+
if (prunePaths.length === 0) return;
|
| 7 |
+
function prune(obj, path) {
|
| 8 |
+
if (!obj || typeof obj !== 'object') return;
|
| 9 |
+
const parts = path.split('.');
|
| 10 |
+
let target = obj;
|
| 11 |
+
for (let i = 0; i < parts.length - 1; i++) {
|
| 12 |
+
const part = parts[i];
|
| 13 |
+
if (part.endsWith('[]')) {
|
| 14 |
+
const key = part.slice(0, -2);
|
| 15 |
+
if (Array.isArray(target[key])) {
|
| 16 |
+
target[key].forEach(item => prune(item, parts.slice(i + 1).join('.')));
|
| 17 |
+
}
|
| 18 |
+
return;
|
| 19 |
+
}
|
| 20 |
+
if (!target[part]) return;
|
| 21 |
+
target = target[part];
|
| 22 |
+
}
|
| 23 |
+
delete target[parts[parts.length - 1]];
|
| 24 |
+
}
|
| 25 |
+
function pruneAll(obj) { for (const p of prunePaths) prune(obj, p); return obj; }
|
| 26 |
+
const jp = JSON.parse;
|
| 27 |
+
JSON.parse = function() { return pruneAll(jp.apply(this, arguments)); };
|
| 28 |
+
const rj = Response.prototype.json;
|
| 29 |
+
Response.prototype.json = function() { return rj.apply(this, arguments).then(pruneAll); };
|
| 30 |
+
})();
|
| 31 |
+
|
| 32 |
+
/// set-constant.js
|
| 33 |
+
(function() {
|
| 34 |
+
'use strict';
|
| 35 |
+
const prop = arguments[0];
|
| 36 |
+
const raw = arguments[1];
|
| 37 |
+
if (!prop) return;
|
| 38 |
+
let value;
|
| 39 |
+
if (raw === 'undefined') value = undefined;
|
| 40 |
+
else if (raw === 'false') value = false;
|
| 41 |
+
else if (raw === 'true') value = true;
|
| 42 |
+
else if (raw === 'null') value = null;
|
| 43 |
+
else value = raw;
|
| 44 |
+
const parts = prop.split('.');
|
| 45 |
+
let obj = window;
|
| 46 |
+
for (let i = 0; i < parts.length - 1; i++) {
|
| 47 |
+
obj = obj[parts[i]] = obj[parts[i]] || {};
|
| 48 |
+
}
|
| 49 |
+
try {
|
| 50 |
+
Object.defineProperty(obj, parts[parts.length - 1], { get: () => value, set: () => {}, configurable: true });
|
| 51 |
+
} catch {}
|
| 52 |
+
})();
|
| 53 |
+
|
| 54 |
+
/// abort-current-script.js
|
| 55 |
+
(function() {
|
| 56 |
+
'use strict';
|
| 57 |
+
const needle = arguments[0] || '';
|
| 58 |
+
const prop = arguments[1] || '';
|
| 59 |
+
if (!needle && !prop) return;
|
| 60 |
+
const re = new RegExp(needle);
|
| 61 |
+
const oe = document.createElement.bind(document);
|
| 62 |
+
document.createElement = function(tag) {
|
| 63 |
+
const el = oe(tag);
|
| 64 |
+
if (String(tag).toLowerCase() === 'script') {
|
| 65 |
+
const set = el.setAttribute.bind(el);
|
| 66 |
+
el.setAttribute = function(name, value) {
|
| 67 |
+
if (name === 'src' && re.test(String(value))) return;
|
| 68 |
+
return set(name, value);
|
| 69 |
+
};
|
| 70 |
+
}
|
| 71 |
+
return el;
|
| 72 |
+
};
|
| 73 |
+
})();
|
| 74 |
+
|
| 75 |
+
/// prevent-fetch.js
|
| 76 |
+
(function() {
|
| 77 |
+
'use strict';
|
| 78 |
+
const needle = arguments[0] || '';
|
| 79 |
+
if (!needle) return;
|
| 80 |
+
const re = new RegExp(needle);
|
| 81 |
+
const of = window.fetch;
|
| 82 |
+
window.fetch = function(input, init) {
|
| 83 |
+
const url = typeof input === 'string' ? input : input && input.url || '';
|
| 84 |
+
if (re.test(url)) return Promise.resolve(new Response('', { status: 204 }));
|
| 85 |
+
return of.apply(this, arguments);
|
| 86 |
+
};
|
| 87 |
+
})();
|
| 88 |
+
|
| 89 |
+
/// m3u8-prune.js
|
| 90 |
+
(function() {
|
| 91 |
+
'use strict';
|
| 92 |
+
const needle = arguments[0] || 'stitched-ad|advertisement|DATERANGE';
|
| 93 |
+
const re = new RegExp(needle);
|
| 94 |
+
const of = window.fetch;
|
| 95 |
+
window.fetch = function(input, init) {
|
| 96 |
+
const url = typeof input === 'string' ? input : input && input.url || '';
|
| 97 |
+
return of.apply(this, arguments).then(resp => {
|
| 98 |
+
if (!String(url).includes('.m3u8')) return resp;
|
| 99 |
+
return resp.clone().text().then(text => {
|
| 100 |
+
const out = text.split('\n').filter(line => !re.test(line)).join('\n');
|
| 101 |
+
return new Response(out, { status: resp.status, headers: resp.headers });
|
| 102 |
+
}).catch(() => resp);
|
| 103 |
+
});
|
| 104 |
+
};
|
| 105 |
+
})();
|
src-tauri/resources/scripts/adblock_layer1.js
CHANGED
|
@@ -1,141 +1,141 @@
|
|
| 1 |
-
(function() {
|
| 2 |
-
'use strict';
|
| 3 |
-
|
| 4 |
-
const blocked = new Set(window.__MUSE_BLOCKED_DOMAINS__ || []);
|
| 5 |
-
|
| 6 |
-
function extractDomain(url) {
|
| 7 |
-
try {
|
| 8 |
-
const absolute = String(url || '').startsWith('http') ? String(url) : location.origin + String(url || '');
|
| 9 |
-
return new URL(absolute).hostname.replace(/^www\./, '');
|
| 10 |
-
} catch { return null; }
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
function isDomainBlocked(url) {
|
| 14 |
-
if (!url || typeof url !== 'string') return false;
|
| 15 |
-
const domain = extractDomain(url);
|
| 16 |
-
if (!domain) return false;
|
| 17 |
-
const parts = domain.split('.');
|
| 18 |
-
for (let i = 0; i < parts.length - 1; i++) {
|
| 19 |
-
if (blocked.has(parts.slice(i).join('.'))) return true;
|
| 20 |
-
}
|
| 21 |
-
return false;
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
function emptyResponseFor(url) {
|
| 25 |
-
const type = String(url || '').toLowerCase();
|
| 26 |
-
if (type.includes('.json') || type.includes('/api/') || type.includes('youtubei')) {
|
| 27 |
-
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
| 28 |
-
}
|
| 29 |
-
if (type.includes('.js')) {
|
| 30 |
-
return new Response('/* blocked by Muse Shield */', { status: 200, headers: { 'Content-Type': 'application/javascript' } });
|
| 31 |
-
}
|
| 32 |
-
return new Response('', { status: 204 });
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
// Patch fetch
|
| 36 |
-
const _fetch = window.fetch;
|
| 37 |
-
window.fetch = function(input, init) {
|
| 38 |
-
const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
|
| 39 |
-
if (isDomainBlocked(url)) {
|
| 40 |
-
return Promise.resolve(emptyResponseFor(url));
|
| 41 |
-
}
|
| 42 |
-
return _fetch.apply(this, arguments);
|
| 43 |
-
};
|
| 44 |
-
|
| 45 |
-
// Patch XMLHttpRequest
|
| 46 |
-
const _xhrOpen = XMLHttpRequest.prototype.open;
|
| 47 |
-
XMLHttpRequest.prototype.open = function(method, url) {
|
| 48 |
-
this.__muse_blocked = isDomainBlocked(url);
|
| 49 |
-
return _xhrOpen.apply(this, arguments);
|
| 50 |
-
};
|
| 51 |
-
const _xhrSend = XMLHttpRequest.prototype.send;
|
| 52 |
-
XMLHttpRequest.prototype.send = function() {
|
| 53 |
-
if (this.__muse_blocked) {
|
| 54 |
-
Object.defineProperty(this, 'readyState', { get: () => 4 });
|
| 55 |
-
Object.defineProperty(this, 'status', { get: () => 204 });
|
| 56 |
-
Object.defineProperty(this, 'responseText', { get: () => '' });
|
| 57 |
-
Object.defineProperty(this, 'response', { get: () => '' });
|
| 58 |
-
if (typeof this.onload === 'function') setTimeout(() => this.onload(new Event('load')), 0);
|
| 59 |
-
if (typeof this.onreadystatechange === 'function') setTimeout(() => this.onreadystatechange(new Event('readystatechange')), 0);
|
| 60 |
-
return;
|
| 61 |
-
}
|
| 62 |
-
return _xhrSend.apply(this, arguments);
|
| 63 |
-
};
|
| 64 |
-
|
| 65 |
-
// Patch WebSocket
|
| 66 |
-
const _WS = window.WebSocket;
|
| 67 |
-
if (_WS) {
|
| 68 |
-
window.WebSocket = function(url, protocols) {
|
| 69 |
-
if (isDomainBlocked(url)) {
|
| 70 |
-
return { readyState: 3, send() {}, close() {}, addEventListener() {}, removeEventListener() {} };
|
| 71 |
-
}
|
| 72 |
-
return new _WS(url, protocols);
|
| 73 |
-
};
|
| 74 |
-
Object.setPrototypeOf(window.WebSocket, _WS);
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
const placeholderSelectors = [
|
| 78 |
-
'[id^="google_ads_iframe"]', '[id*="google_ads"]', '[id*="googleads"]',
|
| 79 |
-
'[id*="doubleclick"]', '[id*="div-gpt-ad"]', '[id*="gpt_unit"]',
|
| 80 |
-
'[class*="ad-slot"]', '[class*="adSlot"]', '[class*="ads-slot"]',
|
| 81 |
-
'[class*="advert"]', '[class*="sponsor"]', '[class*="promoted"]',
|
| 82 |
-
'[class*="ad-container"]', '[class*="ad_wrapper"]', '[class*="ad-wrapper"]',
|
| 83 |
-
'[aria-label*="advertisement" i]', '[data-ad]', '[data-ad-unit]', '[data-ad-slot]',
|
| 84 |
-
'iframe[src*="doubleclick"]', 'iframe[src*="googlesyndication"]',
|
| 85 |
-
'iframe[src*="adnxs"]', 'iframe[src*="taboola"]', 'iframe[src*="outbrain"]',
|
| 86 |
-
'.ytp-ad-module', '.ytp-ad-overlay-container', '#player-ads', '#masthead-ad',
|
| 87 |
-
'ytd-ad-slot-renderer', 'ytd-promoted-sparkles-web-renderer', 'ytd-display-ad-renderer',
|
| 88 |
-
'ytd-promoted-video-renderer', 'ytd-in-feed-ad-layout-renderer'
|
| 89 |
-
];
|
| 90 |
-
|
| 91 |
-
function hidePlaceholders(root = document) {
|
| 92 |
-
try {
|
| 93 |
-
for (const sel of placeholderSelectors) {
|
| 94 |
-
root.querySelectorAll(sel).forEach(el => {
|
| 95 |
-
el.style.setProperty('display', 'none', 'important');
|
| 96 |
-
el.style.setProperty('visibility', 'hidden', 'important');
|
| 97 |
-
el.setAttribute('data-muse-hidden-ad', 'true');
|
| 98 |
-
});
|
| 99 |
-
}
|
| 100 |
-
// Collapse empty ad-shaped containers
|
| 101 |
-
root.querySelectorAll('div, section, aside').forEach(el => {
|
| 102 |
-
const id = (el.id || '').toLowerCase();
|
| 103 |
-
const cls = (el.className || '').toString().toLowerCase();
|
| 104 |
-
if ((/\bads?\b|advert|sponsor|promoted|dfp|gpt|taboola|outbrain/.test(id + ' ' + cls)) && el.offsetHeight < 260) {
|
| 105 |
-
el.style.setProperty('display', 'none', 'important');
|
| 106 |
-
}
|
| 107 |
-
});
|
| 108 |
-
} catch (_) {}
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
// Initial cosmetic cleanup
|
| 112 |
-
if (document.documentElement) {
|
| 113 |
-
const style = document.createElement('style');
|
| 114 |
-
style.id = '__muse_generic_cosmetic';
|
| 115 |
-
style.textContent = placeholderSelectors.join(',') + '{display:none!important;visibility:hidden!important;min-height:0!important;height:0!important}';
|
| 116 |
-
document.documentElement.appendChild(style);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
// Block tracking images/scripts/iframes added dynamically + cleanup placeholders
|
| 120 |
-
const observer = new MutationObserver((mutations) => {
|
| 121 |
-
for (const mutation of mutations) {
|
| 122 |
-
for (const node of mutation.addedNodes) {
|
| 123 |
-
if (node.nodeType !== 1) continue;
|
| 124 |
-
if (node.tagName === 'SCRIPT' && node.src && isDomainBlocked(node.src)) { node.remove(); continue; }
|
| 125 |
-
if (node.tagName === 'IMG' && node.src && isDomainBlocked(node.src)) {
|
| 126 |
-
node.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
| 127 |
-
node.style.display = 'none'; continue;
|
| 128 |
-
}
|
| 129 |
-
if (node.tagName === 'IFRAME' && node.src && isDomainBlocked(node.src)) { node.remove(); continue; }
|
| 130 |
-
hidePlaceholders(node);
|
| 131 |
-
}
|
| 132 |
-
}
|
| 133 |
-
});
|
| 134 |
-
|
| 135 |
-
function start() {
|
| 136 |
-
hidePlaceholders();
|
| 137 |
-
observer.observe(document.documentElement, { childList: true, subtree: true });
|
| 138 |
-
setInterval(hidePlaceholders, 2000);
|
| 139 |
-
}
|
| 140 |
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true }); else start();
|
| 141 |
-
})();
|
|
|
|
| 1 |
+
(function() {
|
| 2 |
+
'use strict';
|
| 3 |
+
|
| 4 |
+
const blocked = new Set(window.__MUSE_BLOCKED_DOMAINS__ || []);
|
| 5 |
+
|
| 6 |
+
function extractDomain(url) {
|
| 7 |
+
try {
|
| 8 |
+
const absolute = String(url || '').startsWith('http') ? String(url) : location.origin + String(url || '');
|
| 9 |
+
return new URL(absolute).hostname.replace(/^www\./, '');
|
| 10 |
+
} catch { return null; }
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function isDomainBlocked(url) {
|
| 14 |
+
if (!url || typeof url !== 'string') return false;
|
| 15 |
+
const domain = extractDomain(url);
|
| 16 |
+
if (!domain) return false;
|
| 17 |
+
const parts = domain.split('.');
|
| 18 |
+
for (let i = 0; i < parts.length - 1; i++) {
|
| 19 |
+
if (blocked.has(parts.slice(i).join('.'))) return true;
|
| 20 |
+
}
|
| 21 |
+
return false;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function emptyResponseFor(url) {
|
| 25 |
+
const type = String(url || '').toLowerCase();
|
| 26 |
+
if (type.includes('.json') || type.includes('/api/') || type.includes('youtubei')) {
|
| 27 |
+
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
| 28 |
+
}
|
| 29 |
+
if (type.includes('.js')) {
|
| 30 |
+
return new Response('/* blocked by Muse Shield */', { status: 200, headers: { 'Content-Type': 'application/javascript' } });
|
| 31 |
+
}
|
| 32 |
+
return new Response('', { status: 204 });
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Patch fetch
|
| 36 |
+
const _fetch = window.fetch;
|
| 37 |
+
window.fetch = function(input, init) {
|
| 38 |
+
const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
|
| 39 |
+
if (isDomainBlocked(url)) {
|
| 40 |
+
return Promise.resolve(emptyResponseFor(url));
|
| 41 |
+
}
|
| 42 |
+
return _fetch.apply(this, arguments);
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
// Patch XMLHttpRequest
|
| 46 |
+
const _xhrOpen = XMLHttpRequest.prototype.open;
|
| 47 |
+
XMLHttpRequest.prototype.open = function(method, url) {
|
| 48 |
+
this.__muse_blocked = isDomainBlocked(url);
|
| 49 |
+
return _xhrOpen.apply(this, arguments);
|
| 50 |
+
};
|
| 51 |
+
const _xhrSend = XMLHttpRequest.prototype.send;
|
| 52 |
+
XMLHttpRequest.prototype.send = function() {
|
| 53 |
+
if (this.__muse_blocked) {
|
| 54 |
+
Object.defineProperty(this, 'readyState', { get: () => 4 });
|
| 55 |
+
Object.defineProperty(this, 'status', { get: () => 204 });
|
| 56 |
+
Object.defineProperty(this, 'responseText', { get: () => '' });
|
| 57 |
+
Object.defineProperty(this, 'response', { get: () => '' });
|
| 58 |
+
if (typeof this.onload === 'function') setTimeout(() => this.onload(new Event('load')), 0);
|
| 59 |
+
if (typeof this.onreadystatechange === 'function') setTimeout(() => this.onreadystatechange(new Event('readystatechange')), 0);
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
return _xhrSend.apply(this, arguments);
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
// Patch WebSocket
|
| 66 |
+
const _WS = window.WebSocket;
|
| 67 |
+
if (_WS) {
|
| 68 |
+
window.WebSocket = function(url, protocols) {
|
| 69 |
+
if (isDomainBlocked(url)) {
|
| 70 |
+
return { readyState: 3, send() {}, close() {}, addEventListener() {}, removeEventListener() {} };
|
| 71 |
+
}
|
| 72 |
+
return new _WS(url, protocols);
|
| 73 |
+
};
|
| 74 |
+
Object.setPrototypeOf(window.WebSocket, _WS);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const placeholderSelectors = [
|
| 78 |
+
'[id^="google_ads_iframe"]', '[id*="google_ads"]', '[id*="googleads"]',
|
| 79 |
+
'[id*="doubleclick"]', '[id*="div-gpt-ad"]', '[id*="gpt_unit"]',
|
| 80 |
+
'[class*="ad-slot"]', '[class*="adSlot"]', '[class*="ads-slot"]',
|
| 81 |
+
'[class*="advert"]', '[class*="sponsor"]', '[class*="promoted"]',
|
| 82 |
+
'[class*="ad-container"]', '[class*="ad_wrapper"]', '[class*="ad-wrapper"]',
|
| 83 |
+
'[aria-label*="advertisement" i]', '[data-ad]', '[data-ad-unit]', '[data-ad-slot]',
|
| 84 |
+
'iframe[src*="doubleclick"]', 'iframe[src*="googlesyndication"]',
|
| 85 |
+
'iframe[src*="adnxs"]', 'iframe[src*="taboola"]', 'iframe[src*="outbrain"]',
|
| 86 |
+
'.ytp-ad-module', '.ytp-ad-overlay-container', '#player-ads', '#masthead-ad',
|
| 87 |
+
'ytd-ad-slot-renderer', 'ytd-promoted-sparkles-web-renderer', 'ytd-display-ad-renderer',
|
| 88 |
+
'ytd-promoted-video-renderer', 'ytd-in-feed-ad-layout-renderer'
|
| 89 |
+
];
|
| 90 |
+
|
| 91 |
+
function hidePlaceholders(root = document) {
|
| 92 |
+
try {
|
| 93 |
+
for (const sel of placeholderSelectors) {
|
| 94 |
+
root.querySelectorAll(sel).forEach(el => {
|
| 95 |
+
el.style.setProperty('display', 'none', 'important');
|
| 96 |
+
el.style.setProperty('visibility', 'hidden', 'important');
|
| 97 |
+
el.setAttribute('data-muse-hidden-ad', 'true');
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
// Collapse empty ad-shaped containers
|
| 101 |
+
root.querySelectorAll('div, section, aside').forEach(el => {
|
| 102 |
+
const id = (el.id || '').toLowerCase();
|
| 103 |
+
const cls = (el.className || '').toString().toLowerCase();
|
| 104 |
+
if ((/\bads?\b|advert|sponsor|promoted|dfp|gpt|taboola|outbrain/.test(id + ' ' + cls)) && el.offsetHeight < 260) {
|
| 105 |
+
el.style.setProperty('display', 'none', 'important');
|
| 106 |
+
}
|
| 107 |
+
});
|
| 108 |
+
} catch (_) {}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Initial cosmetic cleanup
|
| 112 |
+
if (document.documentElement) {
|
| 113 |
+
const style = document.createElement('style');
|
| 114 |
+
style.id = '__muse_generic_cosmetic';
|
| 115 |
+
style.textContent = placeholderSelectors.join(',') + '{display:none!important;visibility:hidden!important;min-height:0!important;height:0!important}';
|
| 116 |
+
document.documentElement.appendChild(style);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Block tracking images/scripts/iframes added dynamically + cleanup placeholders
|
| 120 |
+
const observer = new MutationObserver((mutations) => {
|
| 121 |
+
for (const mutation of mutations) {
|
| 122 |
+
for (const node of mutation.addedNodes) {
|
| 123 |
+
if (node.nodeType !== 1) continue;
|
| 124 |
+
if (node.tagName === 'SCRIPT' && node.src && isDomainBlocked(node.src)) { node.remove(); continue; }
|
| 125 |
+
if (node.tagName === 'IMG' && node.src && isDomainBlocked(node.src)) {
|
| 126 |
+
node.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
| 127 |
+
node.style.display = 'none'; continue;
|
| 128 |
+
}
|
| 129 |
+
if (node.tagName === 'IFRAME' && node.src && isDomainBlocked(node.src)) { node.remove(); continue; }
|
| 130 |
+
hidePlaceholders(node);
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
function start() {
|
| 136 |
+
hidePlaceholders();
|
| 137 |
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
| 138 |
+
setInterval(hidePlaceholders, 2000);
|
| 139 |
+
}
|
| 140 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true }); else start();
|
| 141 |
+
})();
|
src-tauri/resources/scripts/autofill_suppress.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Muse Autofill Suppression
|
| 3 |
-
*
|
| 4 |
-
* Disables native browser autofill (WebView2/WKWebView/WebKitGTK) so that
|
| 5 |
-
* the Muse password vault is the only credential source for form fields.
|
| 6 |
-
*
|
| 7 |
-
* Why: System WebView engines may show their own password save/autofill UIs
|
| 8 |
-
* which conflict with our Stronghold-backed vault. We suppress them by:
|
| 9 |
-
* 1. Setting autocomplete="off" on all form/input elements
|
| 10 |
-
* 2. Using autocomplete="new-password" on password fields (tells browsers not to autofill)
|
| 11 |
-
* 3. Adding data-* attributes that third-party password managers check
|
| 12 |
-
* 4. Intercepting dynamically created inputs via MutationObserver
|
| 13 |
-
*
|
| 14 |
-
* Platform notes:
|
| 15 |
-
* - Windows WebView2: general_autofill_enabled(false) is also set via Rust builder.
|
| 16 |
-
* Password save is already off by default (IsPasswordAutosaveEnabled=false).
|
| 17 |
-
* - macOS WKWebView: No public API exists. This JS approach + the Rust
|
| 18 |
-
* .general_autofill_enabled(false) builder call (noop on mac but future-proof).
|
| 19 |
-
* - Linux WebKitGTK: No built-in password manager UI — this is purely defensive.
|
| 20 |
-
*/
|
| 21 |
-
(function() {
|
| 22 |
-
if (window.__museAutofillSuppressed) return;
|
| 23 |
-
window.__museAutofillSuppressed = true;
|
| 24 |
-
|
| 25 |
-
function suppress(el) {
|
| 26 |
-
if (!el || !el.setAttribute) return;
|
| 27 |
-
var tag = el.tagName;
|
| 28 |
-
if (tag === 'FORM') {
|
| 29 |
-
el.setAttribute('autocomplete', 'off');
|
| 30 |
-
} else if (tag === 'INPUT') {
|
| 31 |
-
var type = (el.type || '').toLowerCase();
|
| 32 |
-
if (type === 'password') {
|
| 33 |
-
// "new-password" tells browsers: don't autofill existing passwords
|
| 34 |
-
el.setAttribute('autocomplete', 'new-password');
|
| 35 |
-
} else if (type === 'email' || type === 'text' || type === 'tel') {
|
| 36 |
-
el.setAttribute('autocomplete', 'off');
|
| 37 |
-
}
|
| 38 |
-
// Suppress third-party password managers too
|
| 39 |
-
el.setAttribute('data-lpignore', 'true'); // LastPass
|
| 40 |
-
el.setAttribute('data-form-type', 'other'); // Dashlane
|
| 41 |
-
el.setAttribute('data-1p-ignore', 'true'); // 1Password
|
| 42 |
-
}
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
function suppressAll() {
|
| 46 |
-
document.querySelectorAll('form, input').forEach(suppress);
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
// Initial pass
|
| 50 |
-
if (document.readyState === 'loading') {
|
| 51 |
-
document.addEventListener('DOMContentLoaded', suppressAll);
|
| 52 |
-
} else {
|
| 53 |
-
suppressAll();
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
// Watch for dynamically added forms/inputs (SPAs)
|
| 57 |
-
var observer = new MutationObserver(function(mutations) {
|
| 58 |
-
for (var i = 0; i < mutations.length; i++) {
|
| 59 |
-
var added = mutations[i].addedNodes;
|
| 60 |
-
for (var j = 0; j < added.length; j++) {
|
| 61 |
-
var node = added[j];
|
| 62 |
-
if (node.nodeType !== 1) continue;
|
| 63 |
-
suppress(node);
|
| 64 |
-
if (node.querySelectorAll) {
|
| 65 |
-
node.querySelectorAll('form, input').forEach(suppress);
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
});
|
| 70 |
-
observer.observe(document.documentElement, { childList: true, subtree: true });
|
| 71 |
-
})();
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Muse Autofill Suppression
|
| 3 |
+
*
|
| 4 |
+
* Disables native browser autofill (WebView2/WKWebView/WebKitGTK) so that
|
| 5 |
+
* the Muse password vault is the only credential source for form fields.
|
| 6 |
+
*
|
| 7 |
+
* Why: System WebView engines may show their own password save/autofill UIs
|
| 8 |
+
* which conflict with our Stronghold-backed vault. We suppress them by:
|
| 9 |
+
* 1. Setting autocomplete="off" on all form/input elements
|
| 10 |
+
* 2. Using autocomplete="new-password" on password fields (tells browsers not to autofill)
|
| 11 |
+
* 3. Adding data-* attributes that third-party password managers check
|
| 12 |
+
* 4. Intercepting dynamically created inputs via MutationObserver
|
| 13 |
+
*
|
| 14 |
+
* Platform notes:
|
| 15 |
+
* - Windows WebView2: general_autofill_enabled(false) is also set via Rust builder.
|
| 16 |
+
* Password save is already off by default (IsPasswordAutosaveEnabled=false).
|
| 17 |
+
* - macOS WKWebView: No public API exists. This JS approach + the Rust
|
| 18 |
+
* .general_autofill_enabled(false) builder call (noop on mac but future-proof).
|
| 19 |
+
* - Linux WebKitGTK: No built-in password manager UI — this is purely defensive.
|
| 20 |
+
*/
|
| 21 |
+
(function() {
|
| 22 |
+
if (window.__museAutofillSuppressed) return;
|
| 23 |
+
window.__museAutofillSuppressed = true;
|
| 24 |
+
|
| 25 |
+
function suppress(el) {
|
| 26 |
+
if (!el || !el.setAttribute) return;
|
| 27 |
+
var tag = el.tagName;
|
| 28 |
+
if (tag === 'FORM') {
|
| 29 |
+
el.setAttribute('autocomplete', 'off');
|
| 30 |
+
} else if (tag === 'INPUT') {
|
| 31 |
+
var type = (el.type || '').toLowerCase();
|
| 32 |
+
if (type === 'password') {
|
| 33 |
+
// "new-password" tells browsers: don't autofill existing passwords
|
| 34 |
+
el.setAttribute('autocomplete', 'new-password');
|
| 35 |
+
} else if (type === 'email' || type === 'text' || type === 'tel') {
|
| 36 |
+
el.setAttribute('autocomplete', 'off');
|
| 37 |
+
}
|
| 38 |
+
// Suppress third-party password managers too
|
| 39 |
+
el.setAttribute('data-lpignore', 'true'); // LastPass
|
| 40 |
+
el.setAttribute('data-form-type', 'other'); // Dashlane
|
| 41 |
+
el.setAttribute('data-1p-ignore', 'true'); // 1Password
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function suppressAll() {
|
| 46 |
+
document.querySelectorAll('form, input').forEach(suppress);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Initial pass
|
| 50 |
+
if (document.readyState === 'loading') {
|
| 51 |
+
document.addEventListener('DOMContentLoaded', suppressAll);
|
| 52 |
+
} else {
|
| 53 |
+
suppressAll();
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Watch for dynamically added forms/inputs (SPAs)
|
| 57 |
+
var observer = new MutationObserver(function(mutations) {
|
| 58 |
+
for (var i = 0; i < mutations.length; i++) {
|
| 59 |
+
var added = mutations[i].addedNodes;
|
| 60 |
+
for (var j = 0; j < added.length; j++) {
|
| 61 |
+
var node = added[j];
|
| 62 |
+
if (node.nodeType !== 1) continue;
|
| 63 |
+
suppress(node);
|
| 64 |
+
if (node.querySelectorAll) {
|
| 65 |
+
node.querySelectorAll('form, input').forEach(suppress);
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
});
|
| 70 |
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
| 71 |
+
})();
|
src-tauri/resources/scripts/canvas_noise.js
CHANGED
|
@@ -1,42 +1,42 @@
|
|
| 1 |
-
(function() {
|
| 2 |
-
'use strict';
|
| 3 |
-
|
| 4 |
-
// Add imperceptible noise to canvas fingerprinting attempts
|
| 5 |
-
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
| 6 |
-
const origToBlob = HTMLCanvasElement.prototype.toBlob;
|
| 7 |
-
const origGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
| 8 |
-
|
| 9 |
-
function addNoise(ctx, canvas) {
|
| 10 |
-
try {
|
| 11 |
-
const w = Math.min(canvas.width, 2);
|
| 12 |
-
const h = Math.min(canvas.height, 2);
|
| 13 |
-
const imageData = ctx.getImageData.call(ctx, 0, 0, w, h);
|
| 14 |
-
// Modify one sub-pixel by ±1
|
| 15 |
-
const idx = Math.floor(Math.random() * imageData.data.length);
|
| 16 |
-
imageData.data[idx] = Math.max(0, Math.min(255, imageData.data[idx] + (Math.random() > 0.5 ? 1 : -1)));
|
| 17 |
-
ctx.putImageData(imageData, 0, 0);
|
| 18 |
-
} catch(e) { /* cross-origin or tainted canvas */ }
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
HTMLCanvasElement.prototype.toDataURL = function() {
|
| 22 |
-
const ctx = this.getContext('2d');
|
| 23 |
-
if (ctx) addNoise(ctx, this);
|
| 24 |
-
return origToDataURL.apply(this, arguments);
|
| 25 |
-
};
|
| 26 |
-
|
| 27 |
-
HTMLCanvasElement.prototype.toBlob = function() {
|
| 28 |
-
const ctx = this.getContext('2d');
|
| 29 |
-
if (ctx) addNoise(ctx, this);
|
| 30 |
-
return origToBlob.apply(this, arguments);
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
CanvasRenderingContext2D.prototype.getImageData = function() {
|
| 34 |
-
const result = origGetImageData.apply(this, arguments);
|
| 35 |
-
// Add noise to returned data for fingerprint resistance
|
| 36 |
-
if (result.data.length > 4) {
|
| 37 |
-
const idx = Math.floor(Math.random() * result.data.length);
|
| 38 |
-
result.data[idx] = Math.max(0, Math.min(255, result.data[idx] + (Math.random() > 0.5 ? 1 : -1)));
|
| 39 |
-
}
|
| 40 |
-
return result;
|
| 41 |
-
};
|
| 42 |
-
})();
|
|
|
|
| 1 |
+
(function() {
|
| 2 |
+
'use strict';
|
| 3 |
+
|
| 4 |
+
// Add imperceptible noise to canvas fingerprinting attempts
|
| 5 |
+
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
| 6 |
+
const origToBlob = HTMLCanvasElement.prototype.toBlob;
|
| 7 |
+
const origGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
| 8 |
+
|
| 9 |
+
function addNoise(ctx, canvas) {
|
| 10 |
+
try {
|
| 11 |
+
const w = Math.min(canvas.width, 2);
|
| 12 |
+
const h = Math.min(canvas.height, 2);
|
| 13 |
+
const imageData = ctx.getImageData.call(ctx, 0, 0, w, h);
|
| 14 |
+
// Modify one sub-pixel by ±1
|
| 15 |
+
const idx = Math.floor(Math.random() * imageData.data.length);
|
| 16 |
+
imageData.data[idx] = Math.max(0, Math.min(255, imageData.data[idx] + (Math.random() > 0.5 ? 1 : -1)));
|
| 17 |
+
ctx.putImageData(imageData, 0, 0);
|
| 18 |
+
} catch(e) { /* cross-origin or tainted canvas */ }
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
HTMLCanvasElement.prototype.toDataURL = function() {
|
| 22 |
+
const ctx = this.getContext('2d');
|
| 23 |
+
if (ctx) addNoise(ctx, this);
|
| 24 |
+
return origToDataURL.apply(this, arguments);
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
HTMLCanvasElement.prototype.toBlob = function() {
|
| 28 |
+
const ctx = this.getContext('2d');
|
| 29 |
+
if (ctx) addNoise(ctx, this);
|
| 30 |
+
return origToBlob.apply(this, arguments);
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
CanvasRenderingContext2D.prototype.getImageData = function() {
|
| 34 |
+
const result = origGetImageData.apply(this, arguments);
|
| 35 |
+
// Add noise to returned data for fingerprint resistance
|
| 36 |
+
if (result.data.length > 4) {
|
| 37 |
+
const idx = Math.floor(Math.random() * result.data.length);
|
| 38 |
+
result.data[idx] = Math.max(0, Math.min(255, result.data[idx] + (Math.random() > 0.5 ? 1 : -1)));
|
| 39 |
+
}
|
| 40 |
+
return result;
|
| 41 |
+
};
|
| 42 |
+
})();
|
src-tauri/resources/scripts/cookie_consent.js
CHANGED
|
@@ -1,72 +1,72 @@
|
|
| 1 |
-
(function() {
|
| 2 |
-
'use strict';
|
| 3 |
-
|
| 4 |
-
const rejectSelectors = [
|
| 5 |
-
'[id*="reject"]', '[class*="reject"]',
|
| 6 |
-
'[id*="decline"]', '[class*="decline"]',
|
| 7 |
-
'button[aria-label*="Reject"]', 'button[aria-label*="reject"]',
|
| 8 |
-
'#onetrust-reject-all-handler',
|
| 9 |
-
'.js-cookie-decline',
|
| 10 |
-
'[data-testid="cookie-policy-dialog-reject-button"]',
|
| 11 |
-
'[class*="cookie"] [class*="reject"]',
|
| 12 |
-
'[class*="consent"] [class*="reject"]',
|
| 13 |
-
'[id*="consent"] button[class*="secondary"]',
|
| 14 |
-
'.fc-cta-do-not-consent',
|
| 15 |
-
'#CybotCookiebotDialogBodyButtonDecline',
|
| 16 |
-
'[class*="CookieConsent"] button:last-child',
|
| 17 |
-
];
|
| 18 |
-
|
| 19 |
-
const bannerSelectors = [
|
| 20 |
-
'[class*="cookie-banner"]', '[class*="cookie-notice"]',
|
| 21 |
-
'[class*="consent-banner"]', '[class*="consent-modal"]',
|
| 22 |
-
'[id*="cookie-banner"]', '[id*="cookie-notice"]',
|
| 23 |
-
'#onetrust-banner-sdk', '.fc-consent-root',
|
| 24 |
-
'#CybotCookiebotDialog', '[class*="CookieConsent"]',
|
| 25 |
-
'[class*="gdpr"]', '[id*="gdpr"]',
|
| 26 |
-
];
|
| 27 |
-
|
| 28 |
-
function clickReject() {
|
| 29 |
-
for (const selector of rejectSelectors) {
|
| 30 |
-
const btn = document.querySelector(selector);
|
| 31 |
-
if (btn && btn.offsetParent !== null) {
|
| 32 |
-
btn.click();
|
| 33 |
-
return true;
|
| 34 |
-
}
|
| 35 |
-
}
|
| 36 |
-
return false;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
function hideBanners() {
|
| 40 |
-
for (const selector of bannerSelectors) {
|
| 41 |
-
const el = document.querySelector(selector);
|
| 42 |
-
if (el && el.offsetParent !== null) {
|
| 43 |
-
el.style.display = 'none';
|
| 44 |
-
}
|
| 45 |
-
}
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
function attempt() {
|
| 49 |
-
if (clickReject()) return true;
|
| 50 |
-
hideBanners();
|
| 51 |
-
return false;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
// Try immediately
|
| 55 |
-
if (!attempt()) {
|
| 56 |
-
// Watch for dynamically added consent banners
|
| 57 |
-
const obs = new MutationObserver(() => {
|
| 58 |
-
if (attempt()) obs.disconnect();
|
| 59 |
-
});
|
| 60 |
-
if (document.body) {
|
| 61 |
-
obs.observe(document.body, { childList: true, subtree: true });
|
| 62 |
-
} else {
|
| 63 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 64 |
-
if (!attempt()) {
|
| 65 |
-
obs.observe(document.body, { childList: true, subtree: true });
|
| 66 |
-
}
|
| 67 |
-
});
|
| 68 |
-
}
|
| 69 |
-
// Safety timeout
|
| 70 |
-
setTimeout(() => { attempt(); obs.disconnect(); }, 8000);
|
| 71 |
-
}
|
| 72 |
-
})();
|
|
|
|
| 1 |
+
(function() {
|
| 2 |
+
'use strict';
|
| 3 |
+
|
| 4 |
+
const rejectSelectors = [
|
| 5 |
+
'[id*="reject"]', '[class*="reject"]',
|
| 6 |
+
'[id*="decline"]', '[class*="decline"]',
|
| 7 |
+
'button[aria-label*="Reject"]', 'button[aria-label*="reject"]',
|
| 8 |
+
'#onetrust-reject-all-handler',
|
| 9 |
+
'.js-cookie-decline',
|
| 10 |
+
'[data-testid="cookie-policy-dialog-reject-button"]',
|
| 11 |
+
'[class*="cookie"] [class*="reject"]',
|
| 12 |
+
'[class*="consent"] [class*="reject"]',
|
| 13 |
+
'[id*="consent"] button[class*="secondary"]',
|
| 14 |
+
'.fc-cta-do-not-consent',
|
| 15 |
+
'#CybotCookiebotDialogBodyButtonDecline',
|
| 16 |
+
'[class*="CookieConsent"] button:last-child',
|
| 17 |
+
];
|
| 18 |
+
|
| 19 |
+
const bannerSelectors = [
|
| 20 |
+
'[class*="cookie-banner"]', '[class*="cookie-notice"]',
|
| 21 |
+
'[class*="consent-banner"]', '[class*="consent-modal"]',
|
| 22 |
+
'[id*="cookie-banner"]', '[id*="cookie-notice"]',
|
| 23 |
+
'#onetrust-banner-sdk', '.fc-consent-root',
|
| 24 |
+
'#CybotCookiebotDialog', '[class*="CookieConsent"]',
|
| 25 |
+
'[class*="gdpr"]', '[id*="gdpr"]',
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
function clickReject() {
|
| 29 |
+
for (const selector of rejectSelectors) {
|
| 30 |
+
const btn = document.querySelector(selector);
|
| 31 |
+
if (btn && btn.offsetParent !== null) {
|
| 32 |
+
btn.click();
|
| 33 |
+
return true;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
return false;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function hideBanners() {
|
| 40 |
+
for (const selector of bannerSelectors) {
|
| 41 |
+
const el = document.querySelector(selector);
|
| 42 |
+
if (el && el.offsetParent !== null) {
|
| 43 |
+
el.style.display = 'none';
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function attempt() {
|
| 49 |
+
if (clickReject()) return true;
|
| 50 |
+
hideBanners();
|
| 51 |
+
return false;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Try immediately
|
| 55 |
+
if (!attempt()) {
|
| 56 |
+
// Watch for dynamically added consent banners
|
| 57 |
+
const obs = new MutationObserver(() => {
|
| 58 |
+
if (attempt()) obs.disconnect();
|
| 59 |
+
});
|
| 60 |
+
if (document.body) {
|
| 61 |
+
obs.observe(document.body, { childList: true, subtree: true });
|
| 62 |
+
} else {
|
| 63 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 64 |
+
if (!attempt()) {
|
| 65 |
+
obs.observe(document.body, { childList: true, subtree: true });
|
| 66 |
+
}
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
+
// Safety timeout
|
| 70 |
+
setTimeout(() => { attempt(); obs.disconnect(); }, 8000);
|
| 71 |
+
}
|
| 72 |
+
})();
|
src-tauri/resources/scripts/hover_overlay.js
CHANGED
|
@@ -1,144 +1,144 @@
|
|
| 1 |
-
/* Refstudio hover overlay: + ADD button. Prevents click-through to underlying links. */
|
| 2 |
-
(function () {
|
| 3 |
-
if (window.__muse_hover_v7) return;
|
| 4 |
-
window.__muse_hover_v7 = true;
|
| 5 |
-
|
| 6 |
-
var MIN = 90;
|
| 7 |
-
var activeImg = null;
|
| 8 |
-
var badge = null;
|
| 9 |
-
var showTimer = 0;
|
| 10 |
-
var hideTimer = 0;
|
| 11 |
-
|
| 12 |
-
function injectCSS() {
|
| 13 |
-
if (document.getElementById('__muse_badge_css')) return;
|
| 14 |
-
var s = document.createElement('style'); s.id = '__muse_badge_css';
|
| 15 |
-
s.textContent = '#__muse_add_badge{all:initial;position:fixed;z-index:2147483647;display:flex;align-items:center;gap:4px;padding:7px 14px;background:rgba(255,255,255,0.97);color:#111;border-radius:999px;box-shadow:0 4px 20px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.05);font:700 11px/1 -apple-system,BlinkMacSystemFont,"Inter",sans-serif;cursor:pointer;opacity:0;transform:translateY(4px) scale(0.92);transition:opacity .15s,transform .15s;pointer-events:none;letter-spacing:0.02em;user-select:none}#__muse_add_badge.v{opacity:1;transform:none;pointer-events:auto}#__muse_add_badge:hover{background:#fff;box-shadow:0 6px 28px rgba(0,0,0,0.35),0 0 0 1px rgba(0,0,0,0.08);transform:scale(1.05)}#__muse_add_badge:active{transform:scale(0.94)}#__muse_add_badge svg{width:14px;height:14px;stroke-width:2.5}#__mt7{all:initial;position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(8px);z-index:2147483647;padding:9px 16px;border-radius:999px;background:rgba(20,20,20,.97);border:1px solid rgba(255,255,255,.1);box-shadow:0 10px 36px rgba(0,0,0,.5);font:600 12px/1 -apple-system,sans-serif;color:#fff;opacity:0;transition:opacity .16s,transform .16s;pointer-events:none}#__mt7.s{opacity:1;transform:translateX(-50%) translateY(0)}';
|
| 16 |
-
(document.head || document.documentElement).appendChild(s);
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
function enc(v) { return encodeURIComponent(v == null ? '' : String(v)); }
|
| 20 |
-
function absUrl(s) { try { return new URL(s || '', location.href).href; } catch (_) { return s || ''; } }
|
| 21 |
-
|
| 22 |
-
function toast(m) {
|
| 23 |
-
var t = document.getElementById('__mt7');
|
| 24 |
-
if (!t) { t = document.createElement('div'); t.id = '__mt7'; document.documentElement.appendChild(t); }
|
| 25 |
-
t.textContent = m; t.classList.add('s');
|
| 26 |
-
clearTimeout(t._t); t._t = setTimeout(function () { t.classList.remove('s'); }, 1500);
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
function addToBoard(url, title, w, h) {
|
| 30 |
-
var u = 'muse-action://board?url=' + enc(url) + '&source=' + enc(location.href) + '&title=' + enc(title) + '&w=' + enc(w) + '&h=' + enc(h);
|
| 31 |
-
// Use an iframe to trigger navigation without changing current page
|
| 32 |
-
var frame = document.createElement('iframe');
|
| 33 |
-
frame.style.cssText = 'display:none;width:0;height:0;border:none;position:absolute;';
|
| 34 |
-
document.documentElement.appendChild(frame);
|
| 35 |
-
try { frame.contentWindow.location.href = u; } catch (_) {
|
| 36 |
-
// Fallback: direct navigation (Rust will block it via on_navigation returning false)
|
| 37 |
-
try { window.location.href = u; } catch (_2) {}
|
| 38 |
-
}
|
| 39 |
-
setTimeout(function () { if (frame.parentNode) frame.parentNode.removeChild(frame); }, 200);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
function pos(el, img) {
|
| 43 |
-
var r = img.getBoundingClientRect();
|
| 44 |
-
// Position at top-right corner of the image (where user expects action buttons)
|
| 45 |
-
var x = r.right - 70;
|
| 46 |
-
var y = r.top + 8;
|
| 47 |
-
if (x < r.left + 8) x = r.left + 8;
|
| 48 |
-
if (x > innerWidth - 80) x = innerWidth - 80;
|
| 49 |
-
if (y < 4) y = 4;
|
| 50 |
-
if (y > innerHeight - 40) y = innerHeight - 40;
|
| 51 |
-
el.style.left = x + 'px';
|
| 52 |
-
el.style.top = y + 'px';
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
function hide() {
|
| 56 |
-
if (!badge) return;
|
| 57 |
-
badge.classList.remove('v');
|
| 58 |
-
activeImg = null;
|
| 59 |
-
setTimeout(function () { if (badge && !badge.classList.contains('v')) badge.style.pointerEvents = 'none'; }, 160);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
function show(img) {
|
| 63 |
-
if (img === activeImg && badge && badge.classList.contains('v')) return;
|
| 64 |
-
injectCSS();
|
| 65 |
-
activeImg = img;
|
| 66 |
-
|
| 67 |
-
if (!badge) {
|
| 68 |
-
badge = document.createElement('div');
|
| 69 |
-
badge.id = '__muse_add_badge';
|
| 70 |
-
badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> ADD';
|
| 71 |
-
|
| 72 |
-
// Use mousedown + prevent everything to ensure no link/image click passes through
|
| 73 |
-
badge.addEventListener('mousedown', function (e) {
|
| 74 |
-
e.preventDefault();
|
| 75 |
-
e.stopPropagation();
|
| 76 |
-
e.stopImmediatePropagation();
|
| 77 |
-
}, true);
|
| 78 |
-
|
| 79 |
-
badge.addEventListener('click', function (e) {
|
| 80 |
-
e.preventDefault();
|
| 81 |
-
e.stopPropagation();
|
| 82 |
-
e.stopImmediatePropagation();
|
| 83 |
-
if (!activeImg) return;
|
| 84 |
-
var url = absUrl(activeImg.currentSrc || activeImg.src);
|
| 85 |
-
var title = activeImg.alt || activeImg.title || document.title || 'Reference';
|
| 86 |
-
var w = activeImg.naturalWidth || Math.round(activeImg.getBoundingClientRect().width);
|
| 87 |
-
var h = activeImg.naturalHeight || Math.round(activeImg.getBoundingClientRect().height);
|
| 88 |
-
addToBoard(url, title, w, h);
|
| 89 |
-
toast('✓ Added to Board');
|
| 90 |
-
}, true);
|
| 91 |
-
|
| 92 |
-
// Prevent any pointer event from bubbling to parent links
|
| 93 |
-
badge.addEventListener('pointerdown', function (e) {
|
| 94 |
-
e.stopPropagation();
|
| 95 |
-
e.stopImmediatePropagation();
|
| 96 |
-
}, true);
|
| 97 |
-
|
| 98 |
-
badge.addEventListener('pointerup', function (e) {
|
| 99 |
-
e.stopPropagation();
|
| 100 |
-
e.stopImmediatePropagation();
|
| 101 |
-
}, true);
|
| 102 |
-
|
| 103 |
-
badge.onmouseenter = function () { clearTimeout(hideTimer); };
|
| 104 |
-
badge.onmouseleave = function () { hideTimer = setTimeout(hide, 300); };
|
| 105 |
-
|
| 106 |
-
document.documentElement.appendChild(badge);
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
pos(badge, activeImg);
|
| 110 |
-
badge.offsetHeight;
|
| 111 |
-
badge.classList.add('v');
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
// Only show on IMG elements, not on elements inside links that happen to be images
|
| 115 |
-
function isValidTarget(el) {
|
| 116 |
-
if (!el || el.tagName !== 'IMG') return false;
|
| 117 |
-
var r = el.getBoundingClientRect();
|
| 118 |
-
return r.width >= MIN && r.height >= MIN;
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
document.addEventListener('mouseover', function (e) {
|
| 122 |
-
var t = e.target;
|
| 123 |
-
if (!isValidTarget(t)) return;
|
| 124 |
-
if (t === activeImg) { clearTimeout(hideTimer); return; }
|
| 125 |
-
clearTimeout(hideTimer); clearTimeout(showTimer);
|
| 126 |
-
showTimer = setTimeout(function () { show(t); }, 250);
|
| 127 |
-
}, true);
|
| 128 |
-
|
| 129 |
-
document.addEventListener('mouseout', function (e) {
|
| 130 |
-
var t = e.target;
|
| 131 |
-
if (!t || t.tagName !== 'IMG' || t !== activeImg) return;
|
| 132 |
-
var rel = e.relatedTarget;
|
| 133 |
-
if (badge && (badge === rel || badge.contains && badge.contains(rel))) return;
|
| 134 |
-
clearTimeout(showTimer);
|
| 135 |
-
hideTimer = setTimeout(hide, 350);
|
| 136 |
-
}, true);
|
| 137 |
-
|
| 138 |
-
window.addEventListener('scroll', function () {
|
| 139 |
-
if (!badge || !activeImg) return;
|
| 140 |
-
var r = activeImg.getBoundingClientRect();
|
| 141 |
-
if (r.bottom < -20 || r.top > innerHeight + 20) hide();
|
| 142 |
-
else pos(badge, activeImg);
|
| 143 |
-
}, { passive: true });
|
| 144 |
-
})();
|
|
|
|
| 1 |
+
/* Refstudio hover overlay: + ADD button. Prevents click-through to underlying links. */
|
| 2 |
+
(function () {
|
| 3 |
+
if (window.__muse_hover_v7) return;
|
| 4 |
+
window.__muse_hover_v7 = true;
|
| 5 |
+
|
| 6 |
+
var MIN = 90;
|
| 7 |
+
var activeImg = null;
|
| 8 |
+
var badge = null;
|
| 9 |
+
var showTimer = 0;
|
| 10 |
+
var hideTimer = 0;
|
| 11 |
+
|
| 12 |
+
function injectCSS() {
|
| 13 |
+
if (document.getElementById('__muse_badge_css')) return;
|
| 14 |
+
var s = document.createElement('style'); s.id = '__muse_badge_css';
|
| 15 |
+
s.textContent = '#__muse_add_badge{all:initial;position:fixed;z-index:2147483647;display:flex;align-items:center;gap:4px;padding:7px 14px;background:rgba(255,255,255,0.97);color:#111;border-radius:999px;box-shadow:0 4px 20px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.05);font:700 11px/1 -apple-system,BlinkMacSystemFont,"Inter",sans-serif;cursor:pointer;opacity:0;transform:translateY(4px) scale(0.92);transition:opacity .15s,transform .15s;pointer-events:none;letter-spacing:0.02em;user-select:none}#__muse_add_badge.v{opacity:1;transform:none;pointer-events:auto}#__muse_add_badge:hover{background:#fff;box-shadow:0 6px 28px rgba(0,0,0,0.35),0 0 0 1px rgba(0,0,0,0.08);transform:scale(1.05)}#__muse_add_badge:active{transform:scale(0.94)}#__muse_add_badge svg{width:14px;height:14px;stroke-width:2.5}#__mt7{all:initial;position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(8px);z-index:2147483647;padding:9px 16px;border-radius:999px;background:rgba(20,20,20,.97);border:1px solid rgba(255,255,255,.1);box-shadow:0 10px 36px rgba(0,0,0,.5);font:600 12px/1 -apple-system,sans-serif;color:#fff;opacity:0;transition:opacity .16s,transform .16s;pointer-events:none}#__mt7.s{opacity:1;transform:translateX(-50%) translateY(0)}';
|
| 16 |
+
(document.head || document.documentElement).appendChild(s);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function enc(v) { return encodeURIComponent(v == null ? '' : String(v)); }
|
| 20 |
+
function absUrl(s) { try { return new URL(s || '', location.href).href; } catch (_) { return s || ''; } }
|
| 21 |
+
|
| 22 |
+
function toast(m) {
|
| 23 |
+
var t = document.getElementById('__mt7');
|
| 24 |
+
if (!t) { t = document.createElement('div'); t.id = '__mt7'; document.documentElement.appendChild(t); }
|
| 25 |
+
t.textContent = m; t.classList.add('s');
|
| 26 |
+
clearTimeout(t._t); t._t = setTimeout(function () { t.classList.remove('s'); }, 1500);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
function addToBoard(url, title, w, h) {
|
| 30 |
+
var u = 'muse-action://board?url=' + enc(url) + '&source=' + enc(location.href) + '&title=' + enc(title) + '&w=' + enc(w) + '&h=' + enc(h);
|
| 31 |
+
// Use an iframe to trigger navigation without changing current page
|
| 32 |
+
var frame = document.createElement('iframe');
|
| 33 |
+
frame.style.cssText = 'display:none;width:0;height:0;border:none;position:absolute;';
|
| 34 |
+
document.documentElement.appendChild(frame);
|
| 35 |
+
try { frame.contentWindow.location.href = u; } catch (_) {
|
| 36 |
+
// Fallback: direct navigation (Rust will block it via on_navigation returning false)
|
| 37 |
+
try { window.location.href = u; } catch (_2) {}
|
| 38 |
+
}
|
| 39 |
+
setTimeout(function () { if (frame.parentNode) frame.parentNode.removeChild(frame); }, 200);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function pos(el, img) {
|
| 43 |
+
var r = img.getBoundingClientRect();
|
| 44 |
+
// Position at top-right corner of the image (where user expects action buttons)
|
| 45 |
+
var x = r.right - 70;
|
| 46 |
+
var y = r.top + 8;
|
| 47 |
+
if (x < r.left + 8) x = r.left + 8;
|
| 48 |
+
if (x > innerWidth - 80) x = innerWidth - 80;
|
| 49 |
+
if (y < 4) y = 4;
|
| 50 |
+
if (y > innerHeight - 40) y = innerHeight - 40;
|
| 51 |
+
el.style.left = x + 'px';
|
| 52 |
+
el.style.top = y + 'px';
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function hide() {
|
| 56 |
+
if (!badge) return;
|
| 57 |
+
badge.classList.remove('v');
|
| 58 |
+
activeImg = null;
|
| 59 |
+
setTimeout(function () { if (badge && !badge.classList.contains('v')) badge.style.pointerEvents = 'none'; }, 160);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function show(img) {
|
| 63 |
+
if (img === activeImg && badge && badge.classList.contains('v')) return;
|
| 64 |
+
injectCSS();
|
| 65 |
+
activeImg = img;
|
| 66 |
+
|
| 67 |
+
if (!badge) {
|
| 68 |
+
badge = document.createElement('div');
|
| 69 |
+
badge.id = '__muse_add_badge';
|
| 70 |
+
badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> ADD';
|
| 71 |
+
|
| 72 |
+
// Use mousedown + prevent everything to ensure no link/image click passes through
|
| 73 |
+
badge.addEventListener('mousedown', function (e) {
|
| 74 |
+
e.preventDefault();
|
| 75 |
+
e.stopPropagation();
|
| 76 |
+
e.stopImmediatePropagation();
|
| 77 |
+
}, true);
|
| 78 |
+
|
| 79 |
+
badge.addEventListener('click', function (e) {
|
| 80 |
+
e.preventDefault();
|
| 81 |
+
e.stopPropagation();
|
| 82 |
+
e.stopImmediatePropagation();
|
| 83 |
+
if (!activeImg) return;
|
| 84 |
+
var url = absUrl(activeImg.currentSrc || activeImg.src);
|
| 85 |
+
var title = activeImg.alt || activeImg.title || document.title || 'Reference';
|
| 86 |
+
var w = activeImg.naturalWidth || Math.round(activeImg.getBoundingClientRect().width);
|
| 87 |
+
var h = activeImg.naturalHeight || Math.round(activeImg.getBoundingClientRect().height);
|
| 88 |
+
addToBoard(url, title, w, h);
|
| 89 |
+
toast('✓ Added to Board');
|
| 90 |
+
}, true);
|
| 91 |
+
|
| 92 |
+
// Prevent any pointer event from bubbling to parent links
|
| 93 |
+
badge.addEventListener('pointerdown', function (e) {
|
| 94 |
+
e.stopPropagation();
|
| 95 |
+
e.stopImmediatePropagation();
|
| 96 |
+
}, true);
|
| 97 |
+
|
| 98 |
+
badge.addEventListener('pointerup', function (e) {
|
| 99 |
+
e.stopPropagation();
|
| 100 |
+
e.stopImmediatePropagation();
|
| 101 |
+
}, true);
|
| 102 |
+
|
| 103 |
+
badge.onmouseenter = function () { clearTimeout(hideTimer); };
|
| 104 |
+
badge.onmouseleave = function () { hideTimer = setTimeout(hide, 300); };
|
| 105 |
+
|
| 106 |
+
document.documentElement.appendChild(badge);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
pos(badge, activeImg);
|
| 110 |
+
badge.offsetHeight;
|
| 111 |
+
badge.classList.add('v');
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Only show on IMG elements, not on elements inside links that happen to be images
|
| 115 |
+
function isValidTarget(el) {
|
| 116 |
+
if (!el || el.tagName !== 'IMG') return false;
|
| 117 |
+
var r = el.getBoundingClientRect();
|
| 118 |
+
return r.width >= MIN && r.height >= MIN;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
document.addEventListener('mouseover', function (e) {
|
| 122 |
+
var t = e.target;
|
| 123 |
+
if (!isValidTarget(t)) return;
|
| 124 |
+
if (t === activeImg) { clearTimeout(hideTimer); return; }
|
| 125 |
+
clearTimeout(hideTimer); clearTimeout(showTimer);
|
| 126 |
+
showTimer = setTimeout(function () { show(t); }, 250);
|
| 127 |
+
}, true);
|
| 128 |
+
|
| 129 |
+
document.addEventListener('mouseout', function (e) {
|
| 130 |
+
var t = e.target;
|
| 131 |
+
if (!t || t.tagName !== 'IMG' || t !== activeImg) return;
|
| 132 |
+
var rel = e.relatedTarget;
|
| 133 |
+
if (badge && (badge === rel || badge.contains && badge.contains(rel))) return;
|
| 134 |
+
clearTimeout(showTimer);
|
| 135 |
+
hideTimer = setTimeout(hide, 350);
|
| 136 |
+
}, true);
|
| 137 |
+
|
| 138 |
+
window.addEventListener('scroll', function () {
|
| 139 |
+
if (!badge || !activeImg) return;
|
| 140 |
+
var r = activeImg.getBoundingClientRect();
|
| 141 |
+
if (r.bottom < -20 || r.top > innerHeight + 20) hide();
|
| 142 |
+
else pos(badge, activeImg);
|
| 143 |
+
}, { passive: true });
|
| 144 |
+
})();
|
src-tauri/resources/scripts/vault_detector.js
CHANGED
|
@@ -1,159 +1,159 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Muse Password Vault — Form Detection & Autofill
|
| 3 |
-
*
|
| 4 |
-
* Injected into every child webview page via initialization_script.
|
| 5 |
-
* Detects login forms and signals Rust via muse-action://vault beacon.
|
| 6 |
-
* Autofill is triggered separately by Rust calling window.__museAutofill().
|
| 7 |
-
*
|
| 8 |
-
* ISOLATION: This runs inside the app's own WebView profile, completely
|
| 9 |
-
* separate from the system browser (Edge/Safari/etc). No credential leakage.
|
| 10 |
-
*/
|
| 11 |
-
(function() {
|
| 12 |
-
'use strict';
|
| 13 |
-
if (window.__museVaultDetector) return;
|
| 14 |
-
window.__museVaultDetector = true;
|
| 15 |
-
|
| 16 |
-
// ─── Beacon to Rust via muse-action:// protocol ──────────────────────
|
| 17 |
-
function beacon(action, data) {
|
| 18 |
-
var params = [];
|
| 19 |
-
data.action = action;
|
| 20 |
-
for (var k in data) {
|
| 21 |
-
if (data[k] != null) params.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
|
| 22 |
-
}
|
| 23 |
-
var img = new Image();
|
| 24 |
-
img.src = 'muse-action://vault?' + params.join('&');
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
// ─── Find password and username fields ───────────────────────────────
|
| 28 |
-
function findPasswordFields() {
|
| 29 |
-
return Array.from(document.querySelectorAll('input[type="password"]:not([disabled]):not([hidden])'));
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function findUsernameFor(pwField) {
|
| 33 |
-
var form = pwField.closest('form') || document;
|
| 34 |
-
var inputs = Array.from(form.querySelectorAll('input'));
|
| 35 |
-
var pwIdx = inputs.indexOf(pwField);
|
| 36 |
-
var candidates = inputs.filter(function(el, idx) {
|
| 37 |
-
if (idx >= pwIdx) return false;
|
| 38 |
-
var t = (el.type || '').toLowerCase();
|
| 39 |
-
if (t === 'hidden' || t === 'submit' || t === 'button' || t === 'checkbox' || t === 'radio') return false;
|
| 40 |
-
var n = (el.name + el.id + el.autocomplete).toLowerCase();
|
| 41 |
-
if (t === 'email' || t === 'text' || t === 'tel') return true;
|
| 42 |
-
if (n.match(/user|email|login|acct|phone/)) return true;
|
| 43 |
-
return false;
|
| 44 |
-
});
|
| 45 |
-
return candidates.pop() || null;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
// ─── Capture credentials on form submission ──────────────────────────
|
| 49 |
-
var lastCaptured = null;
|
| 50 |
-
|
| 51 |
-
function captureFromField(pwField) {
|
| 52 |
-
if (!pwField || !pwField.value) return null;
|
| 53 |
-
var userField = findUsernameFor(pwField);
|
| 54 |
-
return { origin: location.origin, username: userField ? userField.value : '', password: pwField.value };
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
function onFormSubmit(e) {
|
| 58 |
-
var form = e.target || e.currentTarget;
|
| 59 |
-
var pw = form.querySelector('input[type="password"]');
|
| 60 |
-
var creds = captureFromField(pw);
|
| 61 |
-
if (creds && creds.password) {
|
| 62 |
-
lastCaptured = creds;
|
| 63 |
-
beacon('save-prompt', creds);
|
| 64 |
-
}
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
function attachFormListeners() {
|
| 68 |
-
document.querySelectorAll('form').forEach(function(form) {
|
| 69 |
-
if (form.__museVault) return;
|
| 70 |
-
form.__museVault = true;
|
| 71 |
-
form.addEventListener('submit', onFormSubmit, true);
|
| 72 |
-
});
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
// ─── SPA detection: intercept network calls while password field has value
|
| 76 |
-
var origFetch = window.fetch;
|
| 77 |
-
window.fetch = function() {
|
| 78 |
-
checkPendingCredentials();
|
| 79 |
-
return origFetch.apply(this, arguments);
|
| 80 |
-
};
|
| 81 |
-
var origXHRSend = XMLHttpRequest.prototype.send;
|
| 82 |
-
XMLHttpRequest.prototype.send = function() {
|
| 83 |
-
checkPendingCredentials();
|
| 84 |
-
return origXHRSend.apply(this, arguments);
|
| 85 |
-
};
|
| 86 |
-
|
| 87 |
-
function checkPendingCredentials() {
|
| 88 |
-
var pwFields = findPasswordFields();
|
| 89 |
-
for (var i = 0; i < pwFields.length; i++) {
|
| 90 |
-
var creds = captureFromField(pwFields[i]);
|
| 91 |
-
if (creds && creds.password && (!lastCaptured || lastCaptured.password !== creds.password)) {
|
| 92 |
-
lastCaptured = creds;
|
| 93 |
-
// Delay — if page navigates away or password field disappears, login likely succeeded
|
| 94 |
-
setTimeout(function() {
|
| 95 |
-
if (lastCaptured) beacon('save-prompt', lastCaptured);
|
| 96 |
-
}, 2000);
|
| 97 |
-
}
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
// ─── Submit button click detection (for forms without <form> element)
|
| 102 |
-
function attachButtonListeners() {
|
| 103 |
-
document.querySelectorAll('button[type="submit"], input[type="submit"], button:not([type])').forEach(function(btn) {
|
| 104 |
-
if (btn.__museVault) return;
|
| 105 |
-
btn.__museVault = true;
|
| 106 |
-
btn.addEventListener('click', function() {
|
| 107 |
-
setTimeout(checkPendingCredentials, 100);
|
| 108 |
-
}, true);
|
| 109 |
-
});
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
// ─── MutationObserver for dynamically created forms (SPAs) ───────────
|
| 113 |
-
var observer = new MutationObserver(function() {
|
| 114 |
-
attachFormListeners();
|
| 115 |
-
attachButtonListeners();
|
| 116 |
-
});
|
| 117 |
-
observer.observe(document.documentElement, { childList: true, subtree: true });
|
| 118 |
-
|
| 119 |
-
// Initial scan
|
| 120 |
-
if (document.readyState === 'loading') {
|
| 121 |
-
document.addEventListener('DOMContentLoaded', function() { attachFormListeners(); attachButtonListeners(); });
|
| 122 |
-
} else {
|
| 123 |
-
attachFormListeners();
|
| 124 |
-
attachButtonListeners();
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
// ─── AUTOFILL: called from Rust via webview.eval() ───────────────────
|
| 128 |
-
// Rust calls: webview.eval("window.__museAutofill('username', 'password')")
|
| 129 |
-
window.__museAutofill = function(username, password) {
|
| 130 |
-
var pwFields = findPasswordFields();
|
| 131 |
-
if (!pwFields.length) return;
|
| 132 |
-
var pwField = pwFields[0];
|
| 133 |
-
var userField = findUsernameFor(pwField);
|
| 134 |
-
|
| 135 |
-
function fill(el, value) {
|
| 136 |
-
if (!el || !value) return;
|
| 137 |
-
// Use native setter to bypass React/Vue/Angular controlled input
|
| 138 |
-
var setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
| 139 |
-
setter.call(el, value);
|
| 140 |
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
| 141 |
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
| 142 |
-
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
if (userField) fill(userField, username);
|
| 146 |
-
fill(pwField, password);
|
| 147 |
-
};
|
| 148 |
-
|
| 149 |
-
// ─── Notify Rust that this page has login fields (for autofill offer) ─
|
| 150 |
-
function notifyLoginPage() {
|
| 151 |
-
var pwFields = findPasswordFields();
|
| 152 |
-
if (pwFields.length > 0) {
|
| 153 |
-
beacon('has-login-form', { origin: location.origin, fields: pwFields.length });
|
| 154 |
-
}
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
// Check after short delay to let SPA render
|
| 158 |
-
setTimeout(notifyLoginPage, 800);
|
| 159 |
-
})();
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Muse Password Vault — Form Detection & Autofill
|
| 3 |
+
*
|
| 4 |
+
* Injected into every child webview page via initialization_script.
|
| 5 |
+
* Detects login forms and signals Rust via muse-action://vault beacon.
|
| 6 |
+
* Autofill is triggered separately by Rust calling window.__museAutofill().
|
| 7 |
+
*
|
| 8 |
+
* ISOLATION: This runs inside the app's own WebView profile, completely
|
| 9 |
+
* separate from the system browser (Edge/Safari/etc). No credential leakage.
|
| 10 |
+
*/
|
| 11 |
+
(function() {
|
| 12 |
+
'use strict';
|
| 13 |
+
if (window.__museVaultDetector) return;
|
| 14 |
+
window.__museVaultDetector = true;
|
| 15 |
+
|
| 16 |
+
// ─── Beacon to Rust via muse-action:// protocol ──────────────────────
|
| 17 |
+
function beacon(action, data) {
|
| 18 |
+
var params = [];
|
| 19 |
+
data.action = action;
|
| 20 |
+
for (var k in data) {
|
| 21 |
+
if (data[k] != null) params.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
|
| 22 |
+
}
|
| 23 |
+
var img = new Image();
|
| 24 |
+
img.src = 'muse-action://vault?' + params.join('&');
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// ─── Find password and username fields ───────────────────────────────
|
| 28 |
+
function findPasswordFields() {
|
| 29 |
+
return Array.from(document.querySelectorAll('input[type="password"]:not([disabled]):not([hidden])'));
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function findUsernameFor(pwField) {
|
| 33 |
+
var form = pwField.closest('form') || document;
|
| 34 |
+
var inputs = Array.from(form.querySelectorAll('input'));
|
| 35 |
+
var pwIdx = inputs.indexOf(pwField);
|
| 36 |
+
var candidates = inputs.filter(function(el, idx) {
|
| 37 |
+
if (idx >= pwIdx) return false;
|
| 38 |
+
var t = (el.type || '').toLowerCase();
|
| 39 |
+
if (t === 'hidden' || t === 'submit' || t === 'button' || t === 'checkbox' || t === 'radio') return false;
|
| 40 |
+
var n = (el.name + el.id + el.autocomplete).toLowerCase();
|
| 41 |
+
if (t === 'email' || t === 'text' || t === 'tel') return true;
|
| 42 |
+
if (n.match(/user|email|login|acct|phone/)) return true;
|
| 43 |
+
return false;
|
| 44 |
+
});
|
| 45 |
+
return candidates.pop() || null;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// ─── Capture credentials on form submission ──────────────────────────
|
| 49 |
+
var lastCaptured = null;
|
| 50 |
+
|
| 51 |
+
function captureFromField(pwField) {
|
| 52 |
+
if (!pwField || !pwField.value) return null;
|
| 53 |
+
var userField = findUsernameFor(pwField);
|
| 54 |
+
return { origin: location.origin, username: userField ? userField.value : '', password: pwField.value };
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function onFormSubmit(e) {
|
| 58 |
+
var form = e.target || e.currentTarget;
|
| 59 |
+
var pw = form.querySelector('input[type="password"]');
|
| 60 |
+
var creds = captureFromField(pw);
|
| 61 |
+
if (creds && creds.password) {
|
| 62 |
+
lastCaptured = creds;
|
| 63 |
+
beacon('save-prompt', creds);
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function attachFormListeners() {
|
| 68 |
+
document.querySelectorAll('form').forEach(function(form) {
|
| 69 |
+
if (form.__museVault) return;
|
| 70 |
+
form.__museVault = true;
|
| 71 |
+
form.addEventListener('submit', onFormSubmit, true);
|
| 72 |
+
});
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// ─── SPA detection: intercept network calls while password field has value
|
| 76 |
+
var origFetch = window.fetch;
|
| 77 |
+
window.fetch = function() {
|
| 78 |
+
checkPendingCredentials();
|
| 79 |
+
return origFetch.apply(this, arguments);
|
| 80 |
+
};
|
| 81 |
+
var origXHRSend = XMLHttpRequest.prototype.send;
|
| 82 |
+
XMLHttpRequest.prototype.send = function() {
|
| 83 |
+
checkPendingCredentials();
|
| 84 |
+
return origXHRSend.apply(this, arguments);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
function checkPendingCredentials() {
|
| 88 |
+
var pwFields = findPasswordFields();
|
| 89 |
+
for (var i = 0; i < pwFields.length; i++) {
|
| 90 |
+
var creds = captureFromField(pwFields[i]);
|
| 91 |
+
if (creds && creds.password && (!lastCaptured || lastCaptured.password !== creds.password)) {
|
| 92 |
+
lastCaptured = creds;
|
| 93 |
+
// Delay — if page navigates away or password field disappears, login likely succeeded
|
| 94 |
+
setTimeout(function() {
|
| 95 |
+
if (lastCaptured) beacon('save-prompt', lastCaptured);
|
| 96 |
+
}, 2000);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// ─── Submit button click detection (for forms without <form> element)
|
| 102 |
+
function attachButtonListeners() {
|
| 103 |
+
document.querySelectorAll('button[type="submit"], input[type="submit"], button:not([type])').forEach(function(btn) {
|
| 104 |
+
if (btn.__museVault) return;
|
| 105 |
+
btn.__museVault = true;
|
| 106 |
+
btn.addEventListener('click', function() {
|
| 107 |
+
setTimeout(checkPendingCredentials, 100);
|
| 108 |
+
}, true);
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// ─── MutationObserver for dynamically created forms (SPAs) ───────────
|
| 113 |
+
var observer = new MutationObserver(function() {
|
| 114 |
+
attachFormListeners();
|
| 115 |
+
attachButtonListeners();
|
| 116 |
+
});
|
| 117 |
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
| 118 |
+
|
| 119 |
+
// Initial scan
|
| 120 |
+
if (document.readyState === 'loading') {
|
| 121 |
+
document.addEventListener('DOMContentLoaded', function() { attachFormListeners(); attachButtonListeners(); });
|
| 122 |
+
} else {
|
| 123 |
+
attachFormListeners();
|
| 124 |
+
attachButtonListeners();
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// ─── AUTOFILL: called from Rust via webview.eval() ───────────────────
|
| 128 |
+
// Rust calls: webview.eval("window.__museAutofill('username', 'password')")
|
| 129 |
+
window.__museAutofill = function(username, password) {
|
| 130 |
+
var pwFields = findPasswordFields();
|
| 131 |
+
if (!pwFields.length) return;
|
| 132 |
+
var pwField = pwFields[0];
|
| 133 |
+
var userField = findUsernameFor(pwField);
|
| 134 |
+
|
| 135 |
+
function fill(el, value) {
|
| 136 |
+
if (!el || !value) return;
|
| 137 |
+
// Use native setter to bypass React/Vue/Angular controlled input
|
| 138 |
+
var setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
| 139 |
+
setter.call(el, value);
|
| 140 |
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
| 141 |
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
| 142 |
+
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
if (userField) fill(userField, username);
|
| 146 |
+
fill(pwField, password);
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
// ─── Notify Rust that this page has login fields (for autofill offer) ─
|
| 150 |
+
function notifyLoginPage() {
|
| 151 |
+
var pwFields = findPasswordFields();
|
| 152 |
+
if (pwFields.length > 0) {
|
| 153 |
+
beacon('has-login-form', { origin: location.origin, fields: pwFields.length });
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Check after short delay to let SPA render
|
| 158 |
+
setTimeout(notifyLoginPage, 800);
|
| 159 |
+
})();
|
src-tauri/resources/scripts/video_ad_scriptlets.js
CHANGED
|
@@ -1,314 +1,314 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* MUSE Video Ad Scriptlets v2
|
| 3 |
-
* Injected via initialization_script BEFORE any page JavaScript runs.
|
| 4 |
-
*
|
| 5 |
-
* UNIVERSAL approach — not YouTube-specific. Works by:
|
| 6 |
-
* 1. Patching JSON.parse/Response.json to prune known ad properties
|
| 7 |
-
* 2. Blocking known ad-serving fetch/XHR patterns
|
| 8 |
-
* 3. Blocking VAST/VPAID/IMA SDK script loading
|
| 9 |
-
* 4. Auto-skipping video ads when detected
|
| 10 |
-
* 5. Collapsing blank ad containers
|
| 11 |
-
*
|
| 12 |
-
* TIMING IS CRITICAL: runs before ANY page JavaScript.
|
| 13 |
-
*/
|
| 14 |
-
(function() {
|
| 15 |
-
'use strict';
|
| 16 |
-
|
| 17 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 18 |
-
// UNIVERSAL JSON PRUNE — removes ad-related properties from ALL parsed JSON
|
| 19 |
-
// Works on YouTube, Dailymotion, Reddit, news sites, any site using JSON APIs
|
| 20 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 21 |
-
|
| 22 |
-
const AD_JSON_KEYS = new Set([
|
| 23 |
-
'adPlacements', 'playerAds', 'adSlots', 'adBreaks', 'adBreakServiceRenderer',
|
| 24 |
-
'promotedSparklesWebRenderer', 'promotedVideoRenderer', 'adRenderer',
|
| 25 |
-
'displayAdRenderer', 'promoted', 'sponsoredAd', 'sponsoredContent',
|
| 26 |
-
'ad_placements', 'player_ads', 'ad_slots', 'ad_breaks',
|
| 27 |
-
'vastUrl', 'vastXml', 'vpaidUrl',
|
| 28 |
-
'adTagUrl', 'adUnit', 'adSource',
|
| 29 |
-
]);
|
| 30 |
-
|
| 31 |
-
const AD_JSON_NESTED = [
|
| 32 |
-
'auxiliaryUi.messageRenderers.enforcementMessageViewModel',
|
| 33 |
-
'overlay.playerBarActionRenderer',
|
| 34 |
-
];
|
| 35 |
-
|
| 36 |
-
function deepPruneAdKeys(obj, depth) {
|
| 37 |
-
if (!obj || typeof obj !== 'object' || depth > 8) return;
|
| 38 |
-
if (Array.isArray(obj)) {
|
| 39 |
-
obj.forEach(item => deepPruneAdKeys(item, depth + 1));
|
| 40 |
-
return;
|
| 41 |
-
}
|
| 42 |
-
for (const key of Object.keys(obj)) {
|
| 43 |
-
if (AD_JSON_KEYS.has(key)) {
|
| 44 |
-
delete obj[key];
|
| 45 |
-
} else if (typeof obj[key] === 'object') {
|
| 46 |
-
deepPruneAdKeys(obj[key], depth + 1);
|
| 47 |
-
}
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
function pruneNested(obj, path) {
|
| 52 |
-
const parts = path.split('.');
|
| 53 |
-
let target = obj;
|
| 54 |
-
for (let i = 0; i < parts.length - 1; i++) {
|
| 55 |
-
if (!target || typeof target !== 'object') return;
|
| 56 |
-
target = target[parts[i]];
|
| 57 |
-
}
|
| 58 |
-
if (target && typeof target === 'object') {
|
| 59 |
-
delete target[parts[parts.length - 1]];
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
// Patch JSON.parse — universal
|
| 64 |
-
const _JSONParse = JSON.parse;
|
| 65 |
-
JSON.parse = function() {
|
| 66 |
-
const result = _JSONParse.apply(this, arguments);
|
| 67 |
-
if (result && typeof result === 'object') {
|
| 68 |
-
deepPruneAdKeys(result, 0);
|
| 69 |
-
for (const path of AD_JSON_NESTED) {
|
| 70 |
-
pruneNested(result, path);
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
return result;
|
| 74 |
-
};
|
| 75 |
-
|
| 76 |
-
// Patch Response.prototype.json — universal for fetch APIs
|
| 77 |
-
const _responseJson = Response.prototype.json;
|
| 78 |
-
Response.prototype.json = function() {
|
| 79 |
-
return _responseJson.apply(this, arguments).then(data => {
|
| 80 |
-
if (data && typeof data === 'object') {
|
| 81 |
-
deepPruneAdKeys(data, 0);
|
| 82 |
-
for (const path of AD_JSON_NESTED) {
|
| 83 |
-
pruneNested(data, path);
|
| 84 |
-
}
|
| 85 |
-
}
|
| 86 |
-
return data;
|
| 87 |
-
});
|
| 88 |
-
};
|
| 89 |
-
|
| 90 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 91 |
-
// UNIVERSAL FETCH/XHR BLOCK — blocks ad-related network requests on all sites
|
| 92 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 93 |
-
|
| 94 |
-
const AD_URL_PATTERNS = [
|
| 95 |
-
'/pagead/', '/ptracking', '/api/stats/ads', '/ad_break',
|
| 96 |
-
'doubleclick.net', 'googlesyndication.com', 'googleadservices.com',
|
| 97 |
-
'imasdk.googleapis.com', 'securepubads.g.doubleclick.net',
|
| 98 |
-
'amazon-adsystem.com', 'twitchsvc.net',
|
| 99 |
-
'/ads/bid', '/ads/log', '/ads/event',
|
| 100 |
-
'ad.doubleclick.net', 'pagead2.googlesyndication.com',
|
| 101 |
-
'stats.g.doubleclick.net', '2mdn.net',
|
| 102 |
-
'/api/stats/qoe?adformat', 'play.google.com/log',
|
| 103 |
-
'video-ad-stats', '/adunit', '/vast/',
|
| 104 |
-
];
|
| 105 |
-
|
| 106 |
-
function isAdUrl(url) {
|
| 107 |
-
if (!url) return false;
|
| 108 |
-
return AD_URL_PATTERNS.some(p => url.includes(p));
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
const _fetch = window.fetch;
|
| 112 |
-
window.fetch = function(input, init) {
|
| 113 |
-
const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
|
| 114 |
-
if (isAdUrl(url)) {
|
| 115 |
-
return Promise.resolve(new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
| 116 |
-
}
|
| 117 |
-
return _fetch.apply(this, arguments);
|
| 118 |
-
};
|
| 119 |
-
|
| 120 |
-
const _xhrOpen = XMLHttpRequest.prototype.open;
|
| 121 |
-
XMLHttpRequest.prototype.open = function(method, url) {
|
| 122 |
-
this.__muse_blocked = isAdUrl(url);
|
| 123 |
-
return _xhrOpen.apply(this, arguments);
|
| 124 |
-
};
|
| 125 |
-
const _xhrSend = XMLHttpRequest.prototype.send;
|
| 126 |
-
XMLHttpRequest.prototype.send = function() {
|
| 127 |
-
if (this.__muse_blocked) {
|
| 128 |
-
Object.defineProperty(this, 'readyState', { get: () => 4, configurable: true });
|
| 129 |
-
Object.defineProperty(this, 'status', { get: () => 200, configurable: true });
|
| 130 |
-
Object.defineProperty(this, 'responseText', { get: () => '{}', configurable: true });
|
| 131 |
-
Object.defineProperty(this, 'response', { get: () => '{}', configurable: true });
|
| 132 |
-
setTimeout(() => { if (typeof this.onload === 'function') this.onload(new Event('load')); }, 0);
|
| 133 |
-
return;
|
| 134 |
-
}
|
| 135 |
-
return _xhrSend.apply(this, arguments);
|
| 136 |
-
};
|
| 137 |
-
|
| 138 |
-
// ══════════════════════════════════════════════════════════════════════════════
|
| 139 |
-
// UNIVERSAL SCRIPT BLOCK — prevent ad SDK loading on all sites
|
| 140 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 141 |
-
|
| 142 |
-
const AD_SCRIPT_PATTERNS = [
|
| 143 |
-
'imasdk.googleapis.com', 'securepubads', 'pagead2.googlesyndication',
|
| 144 |
-
'adsbygoogle', 'googletag', 'gpt.js', 'pubads',
|
| 145 |
-
'amazon-adsystem.com/aax', 'moat', 'doubleclick.net/tag',
|
| 146 |
-
];
|
| 147 |
-
|
| 148 |
-
const _createElement = document.createElement.bind(document);
|
| 149 |
-
document.createElement = function(tag) {
|
| 150 |
-
const el = _createElement(tag);
|
| 151 |
-
if (tag.toLowerCase() === 'script') {
|
| 152 |
-
const _setSrc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src')?.set;
|
| 153 |
-
if (_setSrc) {
|
| 154 |
-
Object.defineProperty(el, 'src', {
|
| 155 |
-
set(value) {
|
| 156 |
-
if (typeof value === 'string' && AD_SCRIPT_PATTERNS.some(p => value.includes(p))) {
|
| 157 |
-
return; // silently block
|
| 158 |
-
}
|
| 159 |
-
_setSrc.call(this, value);
|
| 160 |
-
},
|
| 161 |
-
get() { return el.getAttribute('src') || ''; },
|
| 162 |
-
configurable: true,
|
| 163 |
-
});
|
| 164 |
-
}
|
| 165 |
-
}
|
| 166 |
-
return el;
|
| 167 |
-
};
|
| 168 |
-
|
| 169 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 170 |
-
// VIDEO AD SKIP — auto-skip/fast-forward video ads when detected (all sites)
|
| 171 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 172 |
-
|
| 173 |
-
function setupVideoAdSkipper() {
|
| 174 |
-
const skipSelectors = [
|
| 175 |
-
'.ytp-ad-skip-button', '.ytp-ad-skip-button-modern', '.ytp-skip-ad-button',
|
| 176 |
-
'[class*="skip-button"]', '[class*="skipButton"]', '[class*="ad-skip"]',
|
| 177 |
-
'button[class*="skip"]',
|
| 178 |
-
];
|
| 179 |
-
|
| 180 |
-
const adContainerSelectors = [
|
| 181 |
-
'.ytp-ad-module', '.ytp-ad-overlay-container', '.ytp-ad-text-overlay',
|
| 182 |
-
'#player-ads', '#masthead-ad', '.ytd-promoted-sparkles-web-renderer',
|
| 183 |
-
'.ytd-display-ad-renderer', '.ytd-promoted-video-renderer',
|
| 184 |
-
'.ytd-ad-slot-renderer', '.ytd-in-feed-ad-layout-renderer',
|
| 185 |
-
'[class*="ad-container"]', '[class*="ad-banner"]', '[class*="ad-slot"]',
|
| 186 |
-
'.video-ads', '.ad-container', '#ad-display',
|
| 187 |
-
];
|
| 188 |
-
|
| 189 |
-
const observer = new MutationObserver(() => {
|
| 190 |
-
// Auto-click skip buttons
|
| 191 |
-
for (const sel of skipSelectors) {
|
| 192 |
-
const btn = document.querySelector(sel);
|
| 193 |
-
if (btn && btn.offsetParent !== null) {
|
| 194 |
-
btn.click();
|
| 195 |
-
}
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
// Collapse ad containers (remove blank spaces)
|
| 199 |
-
for (const sel of adContainerSelectors) {
|
| 200 |
-
document.querySelectorAll(sel).forEach(el => {
|
| 201 |
-
if (el.offsetHeight > 0) {
|
| 202 |
-
el.style.display = 'none';
|
| 203 |
-
el.style.height = '0';
|
| 204 |
-
el.style.overflow = 'hidden';
|
| 205 |
-
el.style.margin = '0';
|
| 206 |
-
el.style.padding = '0';
|
| 207 |
-
}
|
| 208 |
-
});
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
// Fast-forward video ads (player showing ad state)
|
| 212 |
-
const player = document.querySelector('.html5-video-player');
|
| 213 |
-
if (player && player.classList.contains('ad-showing')) {
|
| 214 |
-
player.classList.remove('ad-showing');
|
| 215 |
-
const video = player.querySelector('video');
|
| 216 |
-
if (video && video.duration && isFinite(video.duration) && video.duration < 120) {
|
| 217 |
-
video.currentTime = video.duration;
|
| 218 |
-
video.playbackRate = 16;
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
});
|
| 222 |
-
|
| 223 |
-
function startObserving() {
|
| 224 |
-
if (document.body) {
|
| 225 |
-
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
| 226 |
-
} else {
|
| 227 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 228 |
-
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
| 229 |
-
});
|
| 230 |
-
}
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
startObserving();
|
| 234 |
-
|
| 235 |
-
// Also run periodically to catch delayed ads
|
| 236 |
-
setInterval(() => {
|
| 237 |
-
for (const sel of adContainerSelectors) {
|
| 238 |
-
document.querySelectorAll(sel).forEach(el => {
|
| 239 |
-
el.style.display = 'none';
|
| 240 |
-
});
|
| 241 |
-
}
|
| 242 |
-
// Skip any active video ad
|
| 243 |
-
const player = document.querySelector('.html5-video-player.ad-showing');
|
| 244 |
-
if (player) {
|
| 245 |
-
const video = player.querySelector('video');
|
| 246 |
-
if (video && video.duration && isFinite(video.duration)) {
|
| 247 |
-
video.currentTime = video.duration;
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
}, 1000);
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
setupVideoAdSkipper();
|
| 254 |
-
|
| 255 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 256 |
-
// TWITCH HLS MANIFEST PRUNING — remove ad segments from stream
|
| 257 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 258 |
-
|
| 259 |
-
if (location.hostname.includes('twitch.tv')) {
|
| 260 |
-
const origFetch = window.fetch;
|
| 261 |
-
window.fetch = function(input, init) {
|
| 262 |
-
const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
|
| 263 |
-
|
| 264 |
-
if (url.includes('.m3u8')) {
|
| 265 |
-
return origFetch.apply(this, arguments).then(response => {
|
| 266 |
-
const clone = response.clone();
|
| 267 |
-
return clone.text().then(text => {
|
| 268 |
-
if (text.includes('stitched-ad') || text.includes('advertisement')) {
|
| 269 |
-
const cleaned = text.split('\n').filter(line => {
|
| 270 |
-
if (line.includes('#EXT-X-DATERANGE') && (line.includes('stitched-ad') || line.includes('advertisement'))) return false;
|
| 271 |
-
if (line.includes('#EXT-X-SCTE35-OUT')) return false;
|
| 272 |
-
return true;
|
| 273 |
-
}).join('\n');
|
| 274 |
-
return new Response(cleaned, { status: response.status, statusText: response.statusText, headers: response.headers });
|
| 275 |
-
}
|
| 276 |
-
return response;
|
| 277 |
-
});
|
| 278 |
-
});
|
| 279 |
-
}
|
| 280 |
-
return origFetch.apply(this, arguments);
|
| 281 |
-
};
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 285 |
-
// COSMETIC: Inject CSS to collapse ad containers immediately
|
| 286 |
-
// ═══════════════════════════════════════════════════════════════════════════════
|
| 287 |
-
|
| 288 |
-
const adCss = `
|
| 289 |
-
.ytp-ad-module, .ytp-ad-overlay-container, .ytp-ad-text-overlay,
|
| 290 |
-
#player-ads, #masthead-ad, .ytd-promoted-sparkles-web-renderer,
|
| 291 |
-
.ytd-display-ad-renderer, .ytd-promoted-video-renderer,
|
| 292 |
-
.ytd-ad-slot-renderer, .ytd-in-feed-ad-layout-renderer,
|
| 293 |
-
.ytd-banner-promo-renderer, ytd-rich-item-renderer:has(.ytd-ad-slot-renderer),
|
| 294 |
-
[class*="ad-container"], [class*="ad-banner"],
|
| 295 |
-
.video-ads, .ad-container, #ad-display,
|
| 296 |
-
.ad-showing .ytp-ad-player-overlay,
|
| 297 |
-
.ytp-ad-action-interstitial {
|
| 298 |
-
display: none !important;
|
| 299 |
-
height: 0 !important;
|
| 300 |
-
max-height: 0 !important;
|
| 301 |
-
overflow: hidden !important;
|
| 302 |
-
margin: 0 !important;
|
| 303 |
-
padding: 0 !important;
|
| 304 |
-
opacity: 0 !important;
|
| 305 |
-
pointer-events: none !important;
|
| 306 |
-
}
|
| 307 |
-
`;
|
| 308 |
-
|
| 309 |
-
const style = document.createElement('style');
|
| 310 |
-
style.id = '__muse_ad_collapse';
|
| 311 |
-
style.textContent = adCss;
|
| 312 |
-
(document.head || document.documentElement).appendChild(style);
|
| 313 |
-
|
| 314 |
-
})();
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* MUSE Video Ad Scriptlets v2
|
| 3 |
+
* Injected via initialization_script BEFORE any page JavaScript runs.
|
| 4 |
+
*
|
| 5 |
+
* UNIVERSAL approach — not YouTube-specific. Works by:
|
| 6 |
+
* 1. Patching JSON.parse/Response.json to prune known ad properties
|
| 7 |
+
* 2. Blocking known ad-serving fetch/XHR patterns
|
| 8 |
+
* 3. Blocking VAST/VPAID/IMA SDK script loading
|
| 9 |
+
* 4. Auto-skipping video ads when detected
|
| 10 |
+
* 5. Collapsing blank ad containers
|
| 11 |
+
*
|
| 12 |
+
* TIMING IS CRITICAL: runs before ANY page JavaScript.
|
| 13 |
+
*/
|
| 14 |
+
(function() {
|
| 15 |
+
'use strict';
|
| 16 |
+
|
| 17 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 18 |
+
// UNIVERSAL JSON PRUNE — removes ad-related properties from ALL parsed JSON
|
| 19 |
+
// Works on YouTube, Dailymotion, Reddit, news sites, any site using JSON APIs
|
| 20 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 21 |
+
|
| 22 |
+
const AD_JSON_KEYS = new Set([
|
| 23 |
+
'adPlacements', 'playerAds', 'adSlots', 'adBreaks', 'adBreakServiceRenderer',
|
| 24 |
+
'promotedSparklesWebRenderer', 'promotedVideoRenderer', 'adRenderer',
|
| 25 |
+
'displayAdRenderer', 'promoted', 'sponsoredAd', 'sponsoredContent',
|
| 26 |
+
'ad_placements', 'player_ads', 'ad_slots', 'ad_breaks',
|
| 27 |
+
'vastUrl', 'vastXml', 'vpaidUrl',
|
| 28 |
+
'adTagUrl', 'adUnit', 'adSource',
|
| 29 |
+
]);
|
| 30 |
+
|
| 31 |
+
const AD_JSON_NESTED = [
|
| 32 |
+
'auxiliaryUi.messageRenderers.enforcementMessageViewModel',
|
| 33 |
+
'overlay.playerBarActionRenderer',
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
function deepPruneAdKeys(obj, depth) {
|
| 37 |
+
if (!obj || typeof obj !== 'object' || depth > 8) return;
|
| 38 |
+
if (Array.isArray(obj)) {
|
| 39 |
+
obj.forEach(item => deepPruneAdKeys(item, depth + 1));
|
| 40 |
+
return;
|
| 41 |
+
}
|
| 42 |
+
for (const key of Object.keys(obj)) {
|
| 43 |
+
if (AD_JSON_KEYS.has(key)) {
|
| 44 |
+
delete obj[key];
|
| 45 |
+
} else if (typeof obj[key] === 'object') {
|
| 46 |
+
deepPruneAdKeys(obj[key], depth + 1);
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function pruneNested(obj, path) {
|
| 52 |
+
const parts = path.split('.');
|
| 53 |
+
let target = obj;
|
| 54 |
+
for (let i = 0; i < parts.length - 1; i++) {
|
| 55 |
+
if (!target || typeof target !== 'object') return;
|
| 56 |
+
target = target[parts[i]];
|
| 57 |
+
}
|
| 58 |
+
if (target && typeof target === 'object') {
|
| 59 |
+
delete target[parts[parts.length - 1]];
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Patch JSON.parse — universal
|
| 64 |
+
const _JSONParse = JSON.parse;
|
| 65 |
+
JSON.parse = function() {
|
| 66 |
+
const result = _JSONParse.apply(this, arguments);
|
| 67 |
+
if (result && typeof result === 'object') {
|
| 68 |
+
deepPruneAdKeys(result, 0);
|
| 69 |
+
for (const path of AD_JSON_NESTED) {
|
| 70 |
+
pruneNested(result, path);
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
return result;
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
// Patch Response.prototype.json — universal for fetch APIs
|
| 77 |
+
const _responseJson = Response.prototype.json;
|
| 78 |
+
Response.prototype.json = function() {
|
| 79 |
+
return _responseJson.apply(this, arguments).then(data => {
|
| 80 |
+
if (data && typeof data === 'object') {
|
| 81 |
+
deepPruneAdKeys(data, 0);
|
| 82 |
+
for (const path of AD_JSON_NESTED) {
|
| 83 |
+
pruneNested(data, path);
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
return data;
|
| 87 |
+
});
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 91 |
+
// UNIVERSAL FETCH/XHR BLOCK — blocks ad-related network requests on all sites
|
| 92 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 93 |
+
|
| 94 |
+
const AD_URL_PATTERNS = [
|
| 95 |
+
'/pagead/', '/ptracking', '/api/stats/ads', '/ad_break',
|
| 96 |
+
'doubleclick.net', 'googlesyndication.com', 'googleadservices.com',
|
| 97 |
+
'imasdk.googleapis.com', 'securepubads.g.doubleclick.net',
|
| 98 |
+
'amazon-adsystem.com', 'twitchsvc.net',
|
| 99 |
+
'/ads/bid', '/ads/log', '/ads/event',
|
| 100 |
+
'ad.doubleclick.net', 'pagead2.googlesyndication.com',
|
| 101 |
+
'stats.g.doubleclick.net', '2mdn.net',
|
| 102 |
+
'/api/stats/qoe?adformat', 'play.google.com/log',
|
| 103 |
+
'video-ad-stats', '/adunit', '/vast/',
|
| 104 |
+
];
|
| 105 |
+
|
| 106 |
+
function isAdUrl(url) {
|
| 107 |
+
if (!url) return false;
|
| 108 |
+
return AD_URL_PATTERNS.some(p => url.includes(p));
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
const _fetch = window.fetch;
|
| 112 |
+
window.fetch = function(input, init) {
|
| 113 |
+
const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
|
| 114 |
+
if (isAdUrl(url)) {
|
| 115 |
+
return Promise.resolve(new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
| 116 |
+
}
|
| 117 |
+
return _fetch.apply(this, arguments);
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
const _xhrOpen = XMLHttpRequest.prototype.open;
|
| 121 |
+
XMLHttpRequest.prototype.open = function(method, url) {
|
| 122 |
+
this.__muse_blocked = isAdUrl(url);
|
| 123 |
+
return _xhrOpen.apply(this, arguments);
|
| 124 |
+
};
|
| 125 |
+
const _xhrSend = XMLHttpRequest.prototype.send;
|
| 126 |
+
XMLHttpRequest.prototype.send = function() {
|
| 127 |
+
if (this.__muse_blocked) {
|
| 128 |
+
Object.defineProperty(this, 'readyState', { get: () => 4, configurable: true });
|
| 129 |
+
Object.defineProperty(this, 'status', { get: () => 200, configurable: true });
|
| 130 |
+
Object.defineProperty(this, 'responseText', { get: () => '{}', configurable: true });
|
| 131 |
+
Object.defineProperty(this, 'response', { get: () => '{}', configurable: true });
|
| 132 |
+
setTimeout(() => { if (typeof this.onload === 'function') this.onload(new Event('load')); }, 0);
|
| 133 |
+
return;
|
| 134 |
+
}
|
| 135 |
+
return _xhrSend.apply(this, arguments);
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
// ═════════════════════════════��═════════════════════════════════════════════════
|
| 139 |
+
// UNIVERSAL SCRIPT BLOCK — prevent ad SDK loading on all sites
|
| 140 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 141 |
+
|
| 142 |
+
const AD_SCRIPT_PATTERNS = [
|
| 143 |
+
'imasdk.googleapis.com', 'securepubads', 'pagead2.googlesyndication',
|
| 144 |
+
'adsbygoogle', 'googletag', 'gpt.js', 'pubads',
|
| 145 |
+
'amazon-adsystem.com/aax', 'moat', 'doubleclick.net/tag',
|
| 146 |
+
];
|
| 147 |
+
|
| 148 |
+
const _createElement = document.createElement.bind(document);
|
| 149 |
+
document.createElement = function(tag) {
|
| 150 |
+
const el = _createElement(tag);
|
| 151 |
+
if (tag.toLowerCase() === 'script') {
|
| 152 |
+
const _setSrc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src')?.set;
|
| 153 |
+
if (_setSrc) {
|
| 154 |
+
Object.defineProperty(el, 'src', {
|
| 155 |
+
set(value) {
|
| 156 |
+
if (typeof value === 'string' && AD_SCRIPT_PATTERNS.some(p => value.includes(p))) {
|
| 157 |
+
return; // silently block
|
| 158 |
+
}
|
| 159 |
+
_setSrc.call(this, value);
|
| 160 |
+
},
|
| 161 |
+
get() { return el.getAttribute('src') || ''; },
|
| 162 |
+
configurable: true,
|
| 163 |
+
});
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
return el;
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 170 |
+
// VIDEO AD SKIP — auto-skip/fast-forward video ads when detected (all sites)
|
| 171 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 172 |
+
|
| 173 |
+
function setupVideoAdSkipper() {
|
| 174 |
+
const skipSelectors = [
|
| 175 |
+
'.ytp-ad-skip-button', '.ytp-ad-skip-button-modern', '.ytp-skip-ad-button',
|
| 176 |
+
'[class*="skip-button"]', '[class*="skipButton"]', '[class*="ad-skip"]',
|
| 177 |
+
'button[class*="skip"]',
|
| 178 |
+
];
|
| 179 |
+
|
| 180 |
+
const adContainerSelectors = [
|
| 181 |
+
'.ytp-ad-module', '.ytp-ad-overlay-container', '.ytp-ad-text-overlay',
|
| 182 |
+
'#player-ads', '#masthead-ad', '.ytd-promoted-sparkles-web-renderer',
|
| 183 |
+
'.ytd-display-ad-renderer', '.ytd-promoted-video-renderer',
|
| 184 |
+
'.ytd-ad-slot-renderer', '.ytd-in-feed-ad-layout-renderer',
|
| 185 |
+
'[class*="ad-container"]', '[class*="ad-banner"]', '[class*="ad-slot"]',
|
| 186 |
+
'.video-ads', '.ad-container', '#ad-display',
|
| 187 |
+
];
|
| 188 |
+
|
| 189 |
+
const observer = new MutationObserver(() => {
|
| 190 |
+
// Auto-click skip buttons
|
| 191 |
+
for (const sel of skipSelectors) {
|
| 192 |
+
const btn = document.querySelector(sel);
|
| 193 |
+
if (btn && btn.offsetParent !== null) {
|
| 194 |
+
btn.click();
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// Collapse ad containers (remove blank spaces)
|
| 199 |
+
for (const sel of adContainerSelectors) {
|
| 200 |
+
document.querySelectorAll(sel).forEach(el => {
|
| 201 |
+
if (el.offsetHeight > 0) {
|
| 202 |
+
el.style.display = 'none';
|
| 203 |
+
el.style.height = '0';
|
| 204 |
+
el.style.overflow = 'hidden';
|
| 205 |
+
el.style.margin = '0';
|
| 206 |
+
el.style.padding = '0';
|
| 207 |
+
}
|
| 208 |
+
});
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Fast-forward video ads (player showing ad state)
|
| 212 |
+
const player = document.querySelector('.html5-video-player');
|
| 213 |
+
if (player && player.classList.contains('ad-showing')) {
|
| 214 |
+
player.classList.remove('ad-showing');
|
| 215 |
+
const video = player.querySelector('video');
|
| 216 |
+
if (video && video.duration && isFinite(video.duration) && video.duration < 120) {
|
| 217 |
+
video.currentTime = video.duration;
|
| 218 |
+
video.playbackRate = 16;
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
});
|
| 222 |
+
|
| 223 |
+
function startObserving() {
|
| 224 |
+
if (document.body) {
|
| 225 |
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
| 226 |
+
} else {
|
| 227 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 228 |
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
| 229 |
+
});
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
startObserving();
|
| 234 |
+
|
| 235 |
+
// Also run periodically to catch delayed ads
|
| 236 |
+
setInterval(() => {
|
| 237 |
+
for (const sel of adContainerSelectors) {
|
| 238 |
+
document.querySelectorAll(sel).forEach(el => {
|
| 239 |
+
el.style.display = 'none';
|
| 240 |
+
});
|
| 241 |
+
}
|
| 242 |
+
// Skip any active video ad
|
| 243 |
+
const player = document.querySelector('.html5-video-player.ad-showing');
|
| 244 |
+
if (player) {
|
| 245 |
+
const video = player.querySelector('video');
|
| 246 |
+
if (video && video.duration && isFinite(video.duration)) {
|
| 247 |
+
video.currentTime = video.duration;
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
}, 1000);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
setupVideoAdSkipper();
|
| 254 |
+
|
| 255 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 256 |
+
// TWITCH HLS MANIFEST PRUNING — remove ad segments from stream
|
| 257 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 258 |
+
|
| 259 |
+
if (location.hostname.includes('twitch.tv')) {
|
| 260 |
+
const origFetch = window.fetch;
|
| 261 |
+
window.fetch = function(input, init) {
|
| 262 |
+
const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
|
| 263 |
+
|
| 264 |
+
if (url.includes('.m3u8')) {
|
| 265 |
+
return origFetch.apply(this, arguments).then(response => {
|
| 266 |
+
const clone = response.clone();
|
| 267 |
+
return clone.text().then(text => {
|
| 268 |
+
if (text.includes('stitched-ad') || text.includes('advertisement')) {
|
| 269 |
+
const cleaned = text.split('\n').filter(line => {
|
| 270 |
+
if (line.includes('#EXT-X-DATERANGE') && (line.includes('stitched-ad') || line.includes('advertisement'))) return false;
|
| 271 |
+
if (line.includes('#EXT-X-SCTE35-OUT')) return false;
|
| 272 |
+
return true;
|
| 273 |
+
}).join('\n');
|
| 274 |
+
return new Response(cleaned, { status: response.status, statusText: response.statusText, headers: response.headers });
|
| 275 |
+
}
|
| 276 |
+
return response;
|
| 277 |
+
});
|
| 278 |
+
});
|
| 279 |
+
}
|
| 280 |
+
return origFetch.apply(this, arguments);
|
| 281 |
+
};
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 285 |
+
// COSMETIC: Inject CSS to collapse ad containers immediately
|
| 286 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 287 |
+
|
| 288 |
+
const adCss = `
|
| 289 |
+
.ytp-ad-module, .ytp-ad-overlay-container, .ytp-ad-text-overlay,
|
| 290 |
+
#player-ads, #masthead-ad, .ytd-promoted-sparkles-web-renderer,
|
| 291 |
+
.ytd-display-ad-renderer, .ytd-promoted-video-renderer,
|
| 292 |
+
.ytd-ad-slot-renderer, .ytd-in-feed-ad-layout-renderer,
|
| 293 |
+
.ytd-banner-promo-renderer, ytd-rich-item-renderer:has(.ytd-ad-slot-renderer),
|
| 294 |
+
[class*="ad-container"], [class*="ad-banner"],
|
| 295 |
+
.video-ads, .ad-container, #ad-display,
|
| 296 |
+
.ad-showing .ytp-ad-player-overlay,
|
| 297 |
+
.ytp-ad-action-interstitial {
|
| 298 |
+
display: none !important;
|
| 299 |
+
height: 0 !important;
|
| 300 |
+
max-height: 0 !important;
|
| 301 |
+
overflow: hidden !important;
|
| 302 |
+
margin: 0 !important;
|
| 303 |
+
padding: 0 !important;
|
| 304 |
+
opacity: 0 !important;
|
| 305 |
+
pointer-events: none !important;
|
| 306 |
+
}
|
| 307 |
+
`;
|
| 308 |
+
|
| 309 |
+
const style = document.createElement('style');
|
| 310 |
+
style.id = '__muse_ad_collapse';
|
| 311 |
+
style.textContent = adCss;
|
| 312 |
+
(document.head || document.documentElement).appendChild(style);
|
| 313 |
+
|
| 314 |
+
})();
|
src-tauri/resources/scripts/webrtc_protect.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
| 1 |
-
(function() {
|
| 2 |
-
'use strict';
|
| 3 |
-
|
| 4 |
-
// Prevent WebRTC from leaking local IP addresses
|
| 5 |
-
const OriginalRTC = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
|
| 6 |
-
if (!OriginalRTC) return;
|
| 7 |
-
|
| 8 |
-
window.RTCPeerConnection = function(config, constraints) {
|
| 9 |
-
if (config && config.iceServers) {
|
| 10 |
-
config.iceServers = config.iceServers.filter(server => {
|
| 11 |
-
const urls = Array.isArray(server.urls) ? server.urls : [server.urls || server.url];
|
| 12 |
-
return !urls.some(u => u && u.toString().startsWith('stun:'));
|
| 13 |
-
});
|
| 14 |
-
}
|
| 15 |
-
return new OriginalRTC(config, constraints);
|
| 16 |
-
};
|
| 17 |
-
|
| 18 |
-
window.RTCPeerConnection.prototype = OriginalRTC.prototype;
|
| 19 |
-
Object.setPrototypeOf(window.RTCPeerConnection, OriginalRTC);
|
| 20 |
-
|
| 21 |
-
if (window.webkitRTCPeerConnection) {
|
| 22 |
-
window.webkitRTCPeerConnection = window.RTCPeerConnection;
|
| 23 |
-
}
|
| 24 |
-
})();
|
|
|
|
| 1 |
+
(function() {
|
| 2 |
+
'use strict';
|
| 3 |
+
|
| 4 |
+
// Prevent WebRTC from leaking local IP addresses
|
| 5 |
+
const OriginalRTC = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
|
| 6 |
+
if (!OriginalRTC) return;
|
| 7 |
+
|
| 8 |
+
window.RTCPeerConnection = function(config, constraints) {
|
| 9 |
+
if (config && config.iceServers) {
|
| 10 |
+
config.iceServers = config.iceServers.filter(server => {
|
| 11 |
+
const urls = Array.isArray(server.urls) ? server.urls : [server.urls || server.url];
|
| 12 |
+
return !urls.some(u => u && u.toString().startsWith('stun:'));
|
| 13 |
+
});
|
| 14 |
+
}
|
| 15 |
+
return new OriginalRTC(config, constraints);
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
window.RTCPeerConnection.prototype = OriginalRTC.prototype;
|
| 19 |
+
Object.setPrototypeOf(window.RTCPeerConnection, OriginalRTC);
|
| 20 |
+
|
| 21 |
+
if (window.webkitRTCPeerConnection) {
|
| 22 |
+
window.webkitRTCPeerConnection = window.RTCPeerConnection;
|
| 23 |
+
}
|
| 24 |
+
})();
|
src-tauri/src/adblock/commands.rs
CHANGED
|
@@ -1,64 +1,64 @@
|
|
| 1 |
-
use tauri::{AppHandle, Manager};
|
| 2 |
-
use super::engine::{AdBlockState, ShieldReport};
|
| 3 |
-
|
| 4 |
-
#[tauri::command]
|
| 5 |
-
pub fn shield_get_report(app: AppHandle) -> Result<ShieldReport, String> {
|
| 6 |
-
let state = app.state::<AdBlockState>();
|
| 7 |
-
Ok(state.report())
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
#[tauri::command]
|
| 11 |
-
pub fn shield_check_url(app: AppHandle, url: String, source_url: String, request_type: String) -> bool {
|
| 12 |
-
let state = app.state::<AdBlockState>();
|
| 13 |
-
state.should_block(&url, &source_url, &request_type)
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
#[tauri::command]
|
| 17 |
-
pub fn shield_cosmetic_css(app: AppHandle, url: String) -> String {
|
| 18 |
-
let state = app.state::<AdBlockState>();
|
| 19 |
-
state.get_cosmetic_css(&url)
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
#[tauri::command]
|
| 23 |
-
pub fn shield_toggle_domain(app: AppHandle, domain: String, allowed: bool) -> Result<ShieldReport, String> {
|
| 24 |
-
let state = app.state::<AdBlockState>();
|
| 25 |
-
if let Ok(mut allowlist) = state.allowlist.write() {
|
| 26 |
-
if allowed {
|
| 27 |
-
allowlist.insert(domain);
|
| 28 |
-
} else {
|
| 29 |
-
allowlist.remove(&domain);
|
| 30 |
-
}
|
| 31 |
-
}
|
| 32 |
-
Ok(state.report())
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
#[tauri::command]
|
| 36 |
-
pub fn shield_is_allowed(app: AppHandle, domain: String) -> bool {
|
| 37 |
-
let state = app.state::<AdBlockState>();
|
| 38 |
-
state.allowlist.read().map(|a| a.contains(&domain)).unwrap_or(false)
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
#[tauri::command]
|
| 42 |
-
pub async fn shield_update_lists(app: AppHandle) -> Result<ShieldReport, String> {
|
| 43 |
-
super::updater::update_now(&app).await?;
|
| 44 |
-
let state = app.state::<AdBlockState>();
|
| 45 |
-
Ok(state.report())
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
#[tauri::command]
|
| 49 |
-
pub fn shield_add_user_rule(app: AppHandle, rule: String) -> Result<ShieldReport, String> {
|
| 50 |
-
let state = app.state::<AdBlockState>();
|
| 51 |
-
println!("[muse-shield] User rule added: {rule}");
|
| 52 |
-
Ok(state.report())
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
#[tauri::command]
|
| 56 |
-
pub fn shield_list_subscriptions() -> Vec<&'static str> {
|
| 57 |
-
vec![
|
| 58 |
-
"EasyList",
|
| 59 |
-
"EasyPrivacy",
|
| 60 |
-
"Fanboy Annoyances",
|
| 61 |
-
"uBlock Filters",
|
| 62 |
-
"Peter Lowe",
|
| 63 |
-
]
|
| 64 |
-
}
|
|
|
|
| 1 |
+
use tauri::{AppHandle, Manager};
|
| 2 |
+
use super::engine::{AdBlockState, ShieldReport};
|
| 3 |
+
|
| 4 |
+
#[tauri::command]
|
| 5 |
+
pub fn shield_get_report(app: AppHandle) -> Result<ShieldReport, String> {
|
| 6 |
+
let state = app.state::<AdBlockState>();
|
| 7 |
+
Ok(state.report())
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
#[tauri::command]
|
| 11 |
+
pub fn shield_check_url(app: AppHandle, url: String, source_url: String, request_type: String) -> bool {
|
| 12 |
+
let state = app.state::<AdBlockState>();
|
| 13 |
+
state.should_block(&url, &source_url, &request_type)
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
#[tauri::command]
|
| 17 |
+
pub fn shield_cosmetic_css(app: AppHandle, url: String) -> String {
|
| 18 |
+
let state = app.state::<AdBlockState>();
|
| 19 |
+
state.get_cosmetic_css(&url)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
#[tauri::command]
|
| 23 |
+
pub fn shield_toggle_domain(app: AppHandle, domain: String, allowed: bool) -> Result<ShieldReport, String> {
|
| 24 |
+
let state = app.state::<AdBlockState>();
|
| 25 |
+
if let Ok(mut allowlist) = state.allowlist.write() {
|
| 26 |
+
if allowed {
|
| 27 |
+
allowlist.insert(domain);
|
| 28 |
+
} else {
|
| 29 |
+
allowlist.remove(&domain);
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
Ok(state.report())
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
#[tauri::command]
|
| 36 |
+
pub fn shield_is_allowed(app: AppHandle, domain: String) -> bool {
|
| 37 |
+
let state = app.state::<AdBlockState>();
|
| 38 |
+
state.allowlist.read().map(|a| a.contains(&domain)).unwrap_or(false)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
#[tauri::command]
|
| 42 |
+
pub async fn shield_update_lists(app: AppHandle) -> Result<ShieldReport, String> {
|
| 43 |
+
super::updater::update_now(&app).await?;
|
| 44 |
+
let state = app.state::<AdBlockState>();
|
| 45 |
+
Ok(state.report())
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
#[tauri::command]
|
| 49 |
+
pub fn shield_add_user_rule(app: AppHandle, rule: String) -> Result<ShieldReport, String> {
|
| 50 |
+
let state = app.state::<AdBlockState>();
|
| 51 |
+
println!("[muse-shield] User rule added: {rule}");
|
| 52 |
+
Ok(state.report())
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
#[tauri::command]
|
| 56 |
+
pub fn shield_list_subscriptions() -> Vec<&'static str> {
|
| 57 |
+
vec![
|
| 58 |
+
"EasyList",
|
| 59 |
+
"EasyPrivacy",
|
| 60 |
+
"Fanboy Annoyances",
|
| 61 |
+
"uBlock Filters",
|
| 62 |
+
"Peter Lowe",
|
| 63 |
+
]
|
| 64 |
+
}
|
src-tauri/src/adblock/engine.rs
CHANGED
|
@@ -1,154 +1,154 @@
|
|
| 1 |
-
use adblock::{Engine, FilterSet};
|
| 2 |
-
use adblock::lists::ParseOptions;
|
| 3 |
-
use adblock::request::Request;
|
| 4 |
-
use std::collections::HashSet;
|
| 5 |
-
use std::sync::RwLock;
|
| 6 |
-
use std::sync::atomic::{AtomicU64, Ordering};
|
| 7 |
-
use serde::Serialize;
|
| 8 |
-
|
| 9 |
-
/// Thread-safe wrapper around adblock Engine
|
| 10 |
-
pub struct AdBlockState {
|
| 11 |
-
pub engine: RwLock<Engine>,
|
| 12 |
-
pub stats: AdBlockStats,
|
| 13 |
-
pub allowlist: RwLock<HashSet<String>>,
|
| 14 |
-
pub rule_count: AtomicU64,
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
#[derive(Default)]
|
| 18 |
-
pub struct AdBlockStats {
|
| 19 |
-
pub blocked_requests: AtomicU64,
|
| 20 |
-
pub blocked_cosmetic: AtomicU64,
|
| 21 |
-
pub https_upgrades: AtomicU64,
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
#[derive(Debug, Clone, Serialize)]
|
| 25 |
-
pub struct ShieldReport {
|
| 26 |
-
pub blocked_requests: u64,
|
| 27 |
-
pub blocked_cosmetic: u64,
|
| 28 |
-
pub https_upgrades: u64,
|
| 29 |
-
pub engine_rules: usize,
|
| 30 |
-
pub allowlisted_domains: usize,
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
impl AdBlockState {
|
| 34 |
-
pub fn new() -> Self {
|
| 35 |
-
let (engine, count) = build_engine_from_bundled();
|
| 36 |
-
Self {
|
| 37 |
-
engine: RwLock::new(engine),
|
| 38 |
-
stats: AdBlockStats::default(),
|
| 39 |
-
allowlist: RwLock::new(HashSet::new()),
|
| 40 |
-
rule_count: AtomicU64::new(count as u64),
|
| 41 |
-
}
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
pub fn should_block(&self, url: &str, source_url: &str, request_type: &str) -> bool {
|
| 45 |
-
if let Some(domain) = extract_domain(source_url) {
|
| 46 |
-
if let Ok(allowlist) = self.allowlist.read() {
|
| 47 |
-
if allowlist.contains(&domain) {
|
| 48 |
-
return false;
|
| 49 |
-
}
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
let engine = match self.engine.read() {
|
| 54 |
-
Ok(e) => e,
|
| 55 |
-
Err(_) => return false,
|
| 56 |
-
};
|
| 57 |
-
|
| 58 |
-
match Request::new(url, source_url, request_type) {
|
| 59 |
-
Ok(req) => {
|
| 60 |
-
let result = engine.check_network_request(&req);
|
| 61 |
-
if result.matched && result.exception.is_none() {
|
| 62 |
-
self.stats.blocked_requests.fetch_add(1, Ordering::Relaxed);
|
| 63 |
-
true
|
| 64 |
-
} else {
|
| 65 |
-
false
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
Err(_) => false,
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
pub fn get_cosmetic_css(&self, url: &str) -> String {
|
| 73 |
-
let engine = match self.engine.read() {
|
| 74 |
-
Ok(e) => e,
|
| 75 |
-
Err(_) => return String::new(),
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
let resources = engine.url_cosmetic_resources(url);
|
| 79 |
-
if resources.hide_selectors.is_empty() {
|
| 80 |
-
return String::new();
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
let selectors: Vec<&String> = resources.hide_selectors.iter().collect();
|
| 84 |
-
let css = selectors.iter()
|
| 85 |
-
.map(|s| format!("{s}{{display:none!important}}"))
|
| 86 |
-
.collect::<Vec<_>>()
|
| 87 |
-
.join("\n");
|
| 88 |
-
|
| 89 |
-
self.stats.blocked_cosmetic.fetch_add(selectors.len() as u64, Ordering::Relaxed);
|
| 90 |
-
css
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
/// Get injected scriptlets for a URL (json-prune, set-constant, etc.)
|
| 94 |
-
/// Used for YouTube/Twitch/video ad blocking
|
| 95 |
-
pub fn get_injected_script(&self, url: &str) -> String {
|
| 96 |
-
let engine = match self.engine.read() {
|
| 97 |
-
Ok(e) => e,
|
| 98 |
-
Err(_) => return String::new(),
|
| 99 |
-
};
|
| 100 |
-
let resources = engine.url_cosmetic_resources(url);
|
| 101 |
-
resources.injected_script
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
pub fn report(&self) -> ShieldReport {
|
| 105 |
-
let engine_rules = self.rule_count.load(Ordering::Relaxed) as usize;
|
| 106 |
-
let allowlisted = self.allowlist.read().map(|a| a.len()).unwrap_or(0);
|
| 107 |
-
ShieldReport {
|
| 108 |
-
blocked_requests: self.stats.blocked_requests.load(Ordering::Relaxed),
|
| 109 |
-
blocked_cosmetic: self.stats.blocked_cosmetic.load(Ordering::Relaxed),
|
| 110 |
-
https_upgrades: self.stats.https_upgrades.load(Ordering::Relaxed),
|
| 111 |
-
engine_rules,
|
| 112 |
-
allowlisted_domains: allowlisted,
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
pub fn reload_engine(&self, new_engine: Engine, rule_count: usize) {
|
| 117 |
-
if let Ok(mut engine) = self.engine.write() {
|
| 118 |
-
*engine = new_engine;
|
| 119 |
-
}
|
| 120 |
-
self.rule_count.store(rule_count as u64, Ordering::Relaxed);
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
impl Default for AdBlockState {
|
| 125 |
-
fn default() -> Self {
|
| 126 |
-
Self::new()
|
| 127 |
-
}
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
pub fn build_engine_from_bundled() -> (Engine, usize) {
|
| 131 |
-
let mut filter_set = FilterSet::new(false);
|
| 132 |
-
|
| 133 |
-
let lists: &[&str] = &[
|
| 134 |
-
include_str!("../../resources/filters/easylist_mini.txt"),
|
| 135 |
-
include_str!("../../resources/filters/easyprivacy_mini.txt"),
|
| 136 |
-
include_str!("../../resources/filters/annoyances_mini.txt"),
|
| 137 |
-
];
|
| 138 |
-
|
| 139 |
-
let mut count = 0usize;
|
| 140 |
-
for list in lists {
|
| 141 |
-
let _meta = filter_set.add_filter_list(list, ParseOptions::default());
|
| 142 |
-
count += list.lines().filter(|l| !l.starts_with('!') && !l.trim().is_empty()).count();
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
(Engine::from_filter_set(filter_set, true), count)
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
fn extract_domain(url: &str) -> Option<String> {
|
| 149 |
-
url.split("//")
|
| 150 |
-
.nth(1)
|
| 151 |
-
.and_then(|s| s.split('/').next())
|
| 152 |
-
.map(|s| s.split(':').next().unwrap_or(s).to_lowercase())
|
| 153 |
-
.map(|s| s.trim_start_matches("www.").to_string())
|
| 154 |
-
}
|
|
|
|
| 1 |
+
use adblock::{Engine, FilterSet};
|
| 2 |
+
use adblock::lists::ParseOptions;
|
| 3 |
+
use adblock::request::Request;
|
| 4 |
+
use std::collections::HashSet;
|
| 5 |
+
use std::sync::RwLock;
|
| 6 |
+
use std::sync::atomic::{AtomicU64, Ordering};
|
| 7 |
+
use serde::Serialize;
|
| 8 |
+
|
| 9 |
+
/// Thread-safe wrapper around adblock Engine
|
| 10 |
+
pub struct AdBlockState {
|
| 11 |
+
pub engine: RwLock<Engine>,
|
| 12 |
+
pub stats: AdBlockStats,
|
| 13 |
+
pub allowlist: RwLock<HashSet<String>>,
|
| 14 |
+
pub rule_count: AtomicU64,
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
#[derive(Default)]
|
| 18 |
+
pub struct AdBlockStats {
|
| 19 |
+
pub blocked_requests: AtomicU64,
|
| 20 |
+
pub blocked_cosmetic: AtomicU64,
|
| 21 |
+
pub https_upgrades: AtomicU64,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#[derive(Debug, Clone, Serialize)]
|
| 25 |
+
pub struct ShieldReport {
|
| 26 |
+
pub blocked_requests: u64,
|
| 27 |
+
pub blocked_cosmetic: u64,
|
| 28 |
+
pub https_upgrades: u64,
|
| 29 |
+
pub engine_rules: usize,
|
| 30 |
+
pub allowlisted_domains: usize,
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
impl AdBlockState {
|
| 34 |
+
pub fn new() -> Self {
|
| 35 |
+
let (engine, count) = build_engine_from_bundled();
|
| 36 |
+
Self {
|
| 37 |
+
engine: RwLock::new(engine),
|
| 38 |
+
stats: AdBlockStats::default(),
|
| 39 |
+
allowlist: RwLock::new(HashSet::new()),
|
| 40 |
+
rule_count: AtomicU64::new(count as u64),
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
pub fn should_block(&self, url: &str, source_url: &str, request_type: &str) -> bool {
|
| 45 |
+
if let Some(domain) = extract_domain(source_url) {
|
| 46 |
+
if let Ok(allowlist) = self.allowlist.read() {
|
| 47 |
+
if allowlist.contains(&domain) {
|
| 48 |
+
return false;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
let engine = match self.engine.read() {
|
| 54 |
+
Ok(e) => e,
|
| 55 |
+
Err(_) => return false,
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
match Request::new(url, source_url, request_type) {
|
| 59 |
+
Ok(req) => {
|
| 60 |
+
let result = engine.check_network_request(&req);
|
| 61 |
+
if result.matched && result.exception.is_none() {
|
| 62 |
+
self.stats.blocked_requests.fetch_add(1, Ordering::Relaxed);
|
| 63 |
+
true
|
| 64 |
+
} else {
|
| 65 |
+
false
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
Err(_) => false,
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
pub fn get_cosmetic_css(&self, url: &str) -> String {
|
| 73 |
+
let engine = match self.engine.read() {
|
| 74 |
+
Ok(e) => e,
|
| 75 |
+
Err(_) => return String::new(),
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
let resources = engine.url_cosmetic_resources(url);
|
| 79 |
+
if resources.hide_selectors.is_empty() {
|
| 80 |
+
return String::new();
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
let selectors: Vec<&String> = resources.hide_selectors.iter().collect();
|
| 84 |
+
let css = selectors.iter()
|
| 85 |
+
.map(|s| format!("{s}{{display:none!important}}"))
|
| 86 |
+
.collect::<Vec<_>>()
|
| 87 |
+
.join("\n");
|
| 88 |
+
|
| 89 |
+
self.stats.blocked_cosmetic.fetch_add(selectors.len() as u64, Ordering::Relaxed);
|
| 90 |
+
css
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/// Get injected scriptlets for a URL (json-prune, set-constant, etc.)
|
| 94 |
+
/// Used for YouTube/Twitch/video ad blocking
|
| 95 |
+
pub fn get_injected_script(&self, url: &str) -> String {
|
| 96 |
+
let engine = match self.engine.read() {
|
| 97 |
+
Ok(e) => e,
|
| 98 |
+
Err(_) => return String::new(),
|
| 99 |
+
};
|
| 100 |
+
let resources = engine.url_cosmetic_resources(url);
|
| 101 |
+
resources.injected_script
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
pub fn report(&self) -> ShieldReport {
|
| 105 |
+
let engine_rules = self.rule_count.load(Ordering::Relaxed) as usize;
|
| 106 |
+
let allowlisted = self.allowlist.read().map(|a| a.len()).unwrap_or(0);
|
| 107 |
+
ShieldReport {
|
| 108 |
+
blocked_requests: self.stats.blocked_requests.load(Ordering::Relaxed),
|
| 109 |
+
blocked_cosmetic: self.stats.blocked_cosmetic.load(Ordering::Relaxed),
|
| 110 |
+
https_upgrades: self.stats.https_upgrades.load(Ordering::Relaxed),
|
| 111 |
+
engine_rules,
|
| 112 |
+
allowlisted_domains: allowlisted,
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
pub fn reload_engine(&self, new_engine: Engine, rule_count: usize) {
|
| 117 |
+
if let Ok(mut engine) = self.engine.write() {
|
| 118 |
+
*engine = new_engine;
|
| 119 |
+
}
|
| 120 |
+
self.rule_count.store(rule_count as u64, Ordering::Relaxed);
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
impl Default for AdBlockState {
|
| 125 |
+
fn default() -> Self {
|
| 126 |
+
Self::new()
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
pub fn build_engine_from_bundled() -> (Engine, usize) {
|
| 131 |
+
let mut filter_set = FilterSet::new(false);
|
| 132 |
+
|
| 133 |
+
let lists: &[&str] = &[
|
| 134 |
+
include_str!("../../resources/filters/easylist_mini.txt"),
|
| 135 |
+
include_str!("../../resources/filters/easyprivacy_mini.txt"),
|
| 136 |
+
include_str!("../../resources/filters/annoyances_mini.txt"),
|
| 137 |
+
];
|
| 138 |
+
|
| 139 |
+
let mut count = 0usize;
|
| 140 |
+
for list in lists {
|
| 141 |
+
let _meta = filter_set.add_filter_list(list, ParseOptions::default());
|
| 142 |
+
count += list.lines().filter(|l| !l.starts_with('!') && !l.trim().is_empty()).count();
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
(Engine::from_filter_set(filter_set, true), count)
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
fn extract_domain(url: &str) -> Option<String> {
|
| 149 |
+
url.split("//")
|
| 150 |
+
.nth(1)
|
| 151 |
+
.and_then(|s| s.split('/').next())
|
| 152 |
+
.map(|s| s.split(':').next().unwrap_or(s).to_lowercase())
|
| 153 |
+
.map(|s| s.trim_start_matches("www.").to_string())
|
| 154 |
+
}
|
src-tauri/src/adblock/mod.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
pub mod engine;
|
| 2 |
-
pub mod commands;
|
| 3 |
-
pub mod updater;
|
| 4 |
-
pub mod scripts;
|
|
|
|
| 1 |
+
pub mod engine;
|
| 2 |
+
pub mod commands;
|
| 3 |
+
pub mod updater;
|
| 4 |
+
pub mod scripts;
|
src-tauri/src/adblock/scripts.rs
CHANGED
|
@@ -1,71 +1,71 @@
|
|
| 1 |
-
/// Builds the complete initialization script injected into every child webview
|
| 2 |
-
/// before any page JavaScript runs.
|
| 3 |
-
pub fn build_init_script(blocked_domains_json: &str) -> String {
|
| 4 |
-
let adblock_layer1 = include_str!("../../resources/scripts/adblock_layer1.js");
|
| 5 |
-
let cookie_consent = include_str!("../../resources/scripts/cookie_consent.js");
|
| 6 |
-
let webrtc_protect = include_str!("../../resources/scripts/webrtc_protect.js");
|
| 7 |
-
let canvas_noise = include_str!("../../resources/scripts/canvas_noise.js");
|
| 8 |
-
let hover_overlay = hover_overlay_script();
|
| 9 |
-
let vault_detector = vault_detector_script();
|
| 10 |
-
let autofill_suppress = autofill_suppress_script();
|
| 11 |
-
let video_ad_scriptlets = include_str!("../../resources/scripts/video_ad_scriptlets.js");
|
| 12 |
-
|
| 13 |
-
format!(
|
| 14 |
-
r#"(function() {{
|
| 15 |
-
'use strict';
|
| 16 |
-
window.__MUSE_BLOCKED_DOMAINS__ = {blocked_domains_json};
|
| 17 |
-
document.addEventListener('contextmenu', function(e) {{
|
| 18 |
-
e.preventDefault();
|
| 19 |
-
try {{
|
| 20 |
-
const target = e.target;
|
| 21 |
-
const info = {{
|
| 22 |
-
x: e.screenX, y: e.screenY, clientX: e.clientX, clientY: e.clientY,
|
| 23 |
-
tagName: target.tagName, src: target.src || null,
|
| 24 |
-
href: target.closest('a') ? target.closest('a').href : null,
|
| 25 |
-
text: window.getSelection().toString().slice(0, 200) || null,
|
| 26 |
-
pageUrl: location.href,
|
| 27 |
-
}};
|
| 28 |
-
window.__TAURI_INTERNALS__.invoke('browser_context_menu', info).catch(function() {{}});
|
| 29 |
-
}} catch(ex) {{}}
|
| 30 |
-
}}, true);
|
| 31 |
-
}})();
|
| 32 |
-
{video_ad_scriptlets}
|
| 33 |
-
{adblock_layer1}
|
| 34 |
-
{cookie_consent}
|
| 35 |
-
{webrtc_protect}
|
| 36 |
-
{canvas_noise}
|
| 37 |
-
{autofill_suppress}
|
| 38 |
-
{hover_overlay}
|
| 39 |
-
{vault_detector}"#
|
| 40 |
-
)
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
/// Image hover overlay script.
|
| 44 |
-
pub fn hover_overlay_script() -> &'static str {
|
| 45 |
-
include_str!("../../resources/scripts/hover_overlay.js")
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
/// Password vault form detection + autofill script.
|
| 49 |
-
pub fn vault_detector_script() -> &'static str {
|
| 50 |
-
include_str!("../../resources/scripts/vault_detector.js")
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
/// Suppress native browser autofill so Muse vault is the only credential source.
|
| 54 |
-
pub fn autofill_suppress_script() -> &'static str {
|
| 55 |
-
include_str!("../../resources/scripts/autofill_suppress.js")
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
/// Known ad/tracker domains for fast JS-side lookup
|
| 59 |
-
pub fn blocked_domains_json() -> String {
|
| 60 |
-
let domains: &[&str] = &[
|
| 61 |
-
"googlesyndication.com", "doubleclick.net", "google-analytics.com",
|
| 62 |
-
"googletagmanager.com", "facebook.net", "connect.facebook.net",
|
| 63 |
-
"amazon-adsystem.com", "criteo.com", "outbrain.com", "taboola.com",
|
| 64 |
-
"scorecardresearch.com", "quantserve.com", "adnxs.com", "rubiconproject.com",
|
| 65 |
-
"pubmatic.com", "openx.net", "moatads.com", "hotjar.com", "mixpanel.com",
|
| 66 |
-
"segment.io", "segment.com", "amplitude.com", "fullstory.com",
|
| 67 |
-
"onetrust.com", "cookielaw.org", "trustarc.com", "consensu.org",
|
| 68 |
-
"2mdn.net", "adsymptotic.com", "advertising.com", "bluekai.com",
|
| 69 |
-
];
|
| 70 |
-
serde_json::to_string(domains).unwrap_or_else(|_| "[]".to_string())
|
| 71 |
-
}
|
|
|
|
| 1 |
+
/// Builds the complete initialization script injected into every child webview
|
| 2 |
+
/// before any page JavaScript runs.
|
| 3 |
+
pub fn build_init_script(blocked_domains_json: &str) -> String {
|
| 4 |
+
let adblock_layer1 = include_str!("../../resources/scripts/adblock_layer1.js");
|
| 5 |
+
let cookie_consent = include_str!("../../resources/scripts/cookie_consent.js");
|
| 6 |
+
let webrtc_protect = include_str!("../../resources/scripts/webrtc_protect.js");
|
| 7 |
+
let canvas_noise = include_str!("../../resources/scripts/canvas_noise.js");
|
| 8 |
+
let hover_overlay = hover_overlay_script();
|
| 9 |
+
let vault_detector = vault_detector_script();
|
| 10 |
+
let autofill_suppress = autofill_suppress_script();
|
| 11 |
+
let video_ad_scriptlets = include_str!("../../resources/scripts/video_ad_scriptlets.js");
|
| 12 |
+
|
| 13 |
+
format!(
|
| 14 |
+
r#"(function() {{
|
| 15 |
+
'use strict';
|
| 16 |
+
window.__MUSE_BLOCKED_DOMAINS__ = {blocked_domains_json};
|
| 17 |
+
document.addEventListener('contextmenu', function(e) {{
|
| 18 |
+
e.preventDefault();
|
| 19 |
+
try {{
|
| 20 |
+
const target = e.target;
|
| 21 |
+
const info = {{
|
| 22 |
+
x: e.screenX, y: e.screenY, clientX: e.clientX, clientY: e.clientY,
|
| 23 |
+
tagName: target.tagName, src: target.src || null,
|
| 24 |
+
href: target.closest('a') ? target.closest('a').href : null,
|
| 25 |
+
text: window.getSelection().toString().slice(0, 200) || null,
|
| 26 |
+
pageUrl: location.href,
|
| 27 |
+
}};
|
| 28 |
+
window.__TAURI_INTERNALS__.invoke('browser_context_menu', info).catch(function() {{}});
|
| 29 |
+
}} catch(ex) {{}}
|
| 30 |
+
}}, true);
|
| 31 |
+
}})();
|
| 32 |
+
{video_ad_scriptlets}
|
| 33 |
+
{adblock_layer1}
|
| 34 |
+
{cookie_consent}
|
| 35 |
+
{webrtc_protect}
|
| 36 |
+
{canvas_noise}
|
| 37 |
+
{autofill_suppress}
|
| 38 |
+
{hover_overlay}
|
| 39 |
+
{vault_detector}"#
|
| 40 |
+
)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/// Image hover overlay script.
|
| 44 |
+
pub fn hover_overlay_script() -> &'static str {
|
| 45 |
+
include_str!("../../resources/scripts/hover_overlay.js")
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/// Password vault form detection + autofill script.
|
| 49 |
+
pub fn vault_detector_script() -> &'static str {
|
| 50 |
+
include_str!("../../resources/scripts/vault_detector.js")
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/// Suppress native browser autofill so Muse vault is the only credential source.
|
| 54 |
+
pub fn autofill_suppress_script() -> &'static str {
|
| 55 |
+
include_str!("../../resources/scripts/autofill_suppress.js")
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/// Known ad/tracker domains for fast JS-side lookup
|
| 59 |
+
pub fn blocked_domains_json() -> String {
|
| 60 |
+
let domains: &[&str] = &[
|
| 61 |
+
"googlesyndication.com", "doubleclick.net", "google-analytics.com",
|
| 62 |
+
"googletagmanager.com", "facebook.net", "connect.facebook.net",
|
| 63 |
+
"amazon-adsystem.com", "criteo.com", "outbrain.com", "taboola.com",
|
| 64 |
+
"scorecardresearch.com", "quantserve.com", "adnxs.com", "rubiconproject.com",
|
| 65 |
+
"pubmatic.com", "openx.net", "moatads.com", "hotjar.com", "mixpanel.com",
|
| 66 |
+
"segment.io", "segment.com", "amplitude.com", "fullstory.com",
|
| 67 |
+
"onetrust.com", "cookielaw.org", "trustarc.com", "consensu.org",
|
| 68 |
+
"2mdn.net", "adsymptotic.com", "advertising.com", "bluekai.com",
|
| 69 |
+
];
|
| 70 |
+
serde_json::to_string(domains).unwrap_or_else(|_| "[]".to_string())
|
| 71 |
+
}
|
src-tauri/src/adblock/updater.rs
CHANGED
|
@@ -1,103 +1,103 @@
|
|
| 1 |
-
use tauri::{AppHandle, Manager};
|
| 2 |
-
use reqwest::Client;
|
| 3 |
-
use std::time::Duration;
|
| 4 |
-
use adblock::{Engine, FilterSet};
|
| 5 |
-
use adblock::lists::ParseOptions;
|
| 6 |
-
use super::engine::AdBlockState;
|
| 7 |
-
|
| 8 |
-
const FILTER_URLS: &[(&str, &str)] = &[
|
| 9 |
-
("easylist", "https://easylist.to/easylist/easylist.txt"),
|
| 10 |
-
("easyprivacy", "https://easylist.to/easylist/easyprivacy.txt"),
|
| 11 |
-
("fanboy-annoyances", "https://secure.fanboy.co.nz/fanboy-annoyance.txt"),
|
| 12 |
-
("ublock-filters", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"),
|
| 13 |
-
("ublock-privacy", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/privacy.txt"),
|
| 14 |
-
("ublock-unbreak", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/unbreak.txt"),
|
| 15 |
-
("ublock-quick-fixes", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/quick-fixes.txt"),
|
| 16 |
-
];
|
| 17 |
-
|
| 18 |
-
const SCRIPTLETS_URL: &str = "https://raw.githubusercontent.com/gorhill/uBlock/master/assets/resources/scriptlets.js";
|
| 19 |
-
const FALLBACK_SCRIPTLETS: &str = include_str!("../../resources/scriptlets/muse_ubo_compatible_scriptlets.js");
|
| 20 |
-
const UPDATE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
|
| 21 |
-
|
| 22 |
-
pub fn spawn_updater(app: AppHandle) {
|
| 23 |
-
tauri::async_runtime::spawn(async move {
|
| 24 |
-
tokio::time::sleep(Duration::from_secs(30)).await;
|
| 25 |
-
loop {
|
| 26 |
-
if let Err(e) = update_now(&app).await {
|
| 27 |
-
eprintln!("[muse-shield] Filter list update failed: {e}");
|
| 28 |
-
}
|
| 29 |
-
tokio::time::sleep(UPDATE_INTERVAL).await;
|
| 30 |
-
}
|
| 31 |
-
});
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
fn parse_scriptlet_file(path: &std::path::Path) -> Vec<adblock::resources::Resource> {
|
| 35 |
-
#[allow(deprecated)]
|
| 36 |
-
adblock::resources::resource_assembler::assemble_scriptlet_resources(path)
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
pub async fn update_now(app: &AppHandle) -> Result<(), String> {
|
| 40 |
-
let client = Client::builder()
|
| 41 |
-
.timeout(Duration::from_secs(45))
|
| 42 |
-
.user_agent("Muse/0.2 (adblock-updater)")
|
| 43 |
-
.build()
|
| 44 |
-
.map_err(|e| e.to_string())?;
|
| 45 |
-
|
| 46 |
-
let mut filter_set = FilterSet::new(false);
|
| 47 |
-
let mut total_rules = 0usize;
|
| 48 |
-
|
| 49 |
-
for (name, url) in FILTER_URLS {
|
| 50 |
-
match client.get(*url).send().await {
|
| 51 |
-
Ok(resp) => {
|
| 52 |
-
if let Ok(text) = resp.text().await {
|
| 53 |
-
let _meta = filter_set.add_filter_list(&text, ParseOptions::default());
|
| 54 |
-
let rule_count = text.lines().filter(|l| !l.trim().is_empty() && !l.starts_with('!')).count();
|
| 55 |
-
total_rules += rule_count;
|
| 56 |
-
println!("[muse-shield] Updated {name}: {rule_count} rules");
|
| 57 |
-
}
|
| 58 |
-
}
|
| 59 |
-
Err(e) => eprintln!("[muse-shield] Failed to fetch {name}: {e}"),
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
if total_rules == 0 { return Ok(()); }
|
| 64 |
-
|
| 65 |
-
let mut new_engine = Engine::from_filter_set(filter_set, true);
|
| 66 |
-
let cache_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
| 67 |
-
std::fs::create_dir_all(&cache_dir).map_err(|e| e.to_string())?;
|
| 68 |
-
|
| 69 |
-
// 1) Try upstream uBO resource source.
|
| 70 |
-
let mut resources = Vec::new();
|
| 71 |
-
match client.get(SCRIPTLETS_URL).send().await {
|
| 72 |
-
Ok(resp) => {
|
| 73 |
-
if let Ok(scriptlets_js) = resp.text().await {
|
| 74 |
-
let scriptlets_path = cache_dir.join("ubo-scriptlets.js");
|
| 75 |
-
std::fs::write(&scriptlets_path, &scriptlets_js).map_err(|e| e.to_string())?;
|
| 76 |
-
resources = parse_scriptlet_file(&scriptlets_path);
|
| 77 |
-
println!("[muse-shield] Upstream uBO scriptlets parsed {} resources", resources.len());
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
-
Err(e) => eprintln!("[muse-shield] Failed to fetch upstream uBO scriptlets: {e}"),
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
// 2) Deterministic fallback bundle in adblock-rust legacy resource format.
|
| 84 |
-
if resources.is_empty() {
|
| 85 |
-
let fallback_path = cache_dir.join("muse-fallback-scriptlets.js");
|
| 86 |
-
std::fs::write(&fallback_path, FALLBACK_SCRIPTLETS).map_err(|e| e.to_string())?;
|
| 87 |
-
resources = parse_scriptlet_file(&fallback_path);
|
| 88 |
-
println!("[muse-shield] Fallback scriptlets parsed {} resources", resources.len());
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
if !resources.is_empty() {
|
| 92 |
-
let resource_count = resources.len();
|
| 93 |
-
new_engine.use_resources(resources);
|
| 94 |
-
println!("[muse-shield] Loaded {resource_count} scriptlet resources via adblock-rust resource_assembler");
|
| 95 |
-
} else {
|
| 96 |
-
eprintln!("[muse-shield] WARNING: scriptlet resource parsing returned 0 even for fallback bundle. Built-in early video scriptlets remain active.");
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
let state = app.state::<AdBlockState>();
|
| 100 |
-
state.reload_engine(new_engine, total_rules);
|
| 101 |
-
println!("[muse-shield] Engine reloaded with {total_rules} rules + scriptlet resources");
|
| 102 |
-
Ok(())
|
| 103 |
-
}
|
|
|
|
| 1 |
+
use tauri::{AppHandle, Manager};
|
| 2 |
+
use reqwest::Client;
|
| 3 |
+
use std::time::Duration;
|
| 4 |
+
use adblock::{Engine, FilterSet};
|
| 5 |
+
use adblock::lists::ParseOptions;
|
| 6 |
+
use super::engine::AdBlockState;
|
| 7 |
+
|
| 8 |
+
const FILTER_URLS: &[(&str, &str)] = &[
|
| 9 |
+
("easylist", "https://easylist.to/easylist/easylist.txt"),
|
| 10 |
+
("easyprivacy", "https://easylist.to/easylist/easyprivacy.txt"),
|
| 11 |
+
("fanboy-annoyances", "https://secure.fanboy.co.nz/fanboy-annoyance.txt"),
|
| 12 |
+
("ublock-filters", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"),
|
| 13 |
+
("ublock-privacy", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/privacy.txt"),
|
| 14 |
+
("ublock-unbreak", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/unbreak.txt"),
|
| 15 |
+
("ublock-quick-fixes", "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/quick-fixes.txt"),
|
| 16 |
+
];
|
| 17 |
+
|
| 18 |
+
const SCRIPTLETS_URL: &str = "https://raw.githubusercontent.com/gorhill/uBlock/master/assets/resources/scriptlets.js";
|
| 19 |
+
const FALLBACK_SCRIPTLETS: &str = include_str!("../../resources/scriptlets/muse_ubo_compatible_scriptlets.js");
|
| 20 |
+
const UPDATE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
|
| 21 |
+
|
| 22 |
+
pub fn spawn_updater(app: AppHandle) {
|
| 23 |
+
tauri::async_runtime::spawn(async move {
|
| 24 |
+
tokio::time::sleep(Duration::from_secs(30)).await;
|
| 25 |
+
loop {
|
| 26 |
+
if let Err(e) = update_now(&app).await {
|
| 27 |
+
eprintln!("[muse-shield] Filter list update failed: {e}");
|
| 28 |
+
}
|
| 29 |
+
tokio::time::sleep(UPDATE_INTERVAL).await;
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
fn parse_scriptlet_file(path: &std::path::Path) -> Vec<adblock::resources::Resource> {
|
| 35 |
+
#[allow(deprecated)]
|
| 36 |
+
adblock::resources::resource_assembler::assemble_scriptlet_resources(path)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
pub async fn update_now(app: &AppHandle) -> Result<(), String> {
|
| 40 |
+
let client = Client::builder()
|
| 41 |
+
.timeout(Duration::from_secs(45))
|
| 42 |
+
.user_agent("Muse/0.2 (adblock-updater)")
|
| 43 |
+
.build()
|
| 44 |
+
.map_err(|e| e.to_string())?;
|
| 45 |
+
|
| 46 |
+
let mut filter_set = FilterSet::new(false);
|
| 47 |
+
let mut total_rules = 0usize;
|
| 48 |
+
|
| 49 |
+
for (name, url) in FILTER_URLS {
|
| 50 |
+
match client.get(*url).send().await {
|
| 51 |
+
Ok(resp) => {
|
| 52 |
+
if let Ok(text) = resp.text().await {
|
| 53 |
+
let _meta = filter_set.add_filter_list(&text, ParseOptions::default());
|
| 54 |
+
let rule_count = text.lines().filter(|l| !l.trim().is_empty() && !l.starts_with('!')).count();
|
| 55 |
+
total_rules += rule_count;
|
| 56 |
+
println!("[muse-shield] Updated {name}: {rule_count} rules");
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
Err(e) => eprintln!("[muse-shield] Failed to fetch {name}: {e}"),
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if total_rules == 0 { return Ok(()); }
|
| 64 |
+
|
| 65 |
+
let mut new_engine = Engine::from_filter_set(filter_set, true);
|
| 66 |
+
let cache_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
| 67 |
+
std::fs::create_dir_all(&cache_dir).map_err(|e| e.to_string())?;
|
| 68 |
+
|
| 69 |
+
// 1) Try upstream uBO resource source.
|
| 70 |
+
let mut resources = Vec::new();
|
| 71 |
+
match client.get(SCRIPTLETS_URL).send().await {
|
| 72 |
+
Ok(resp) => {
|
| 73 |
+
if let Ok(scriptlets_js) = resp.text().await {
|
| 74 |
+
let scriptlets_path = cache_dir.join("ubo-scriptlets.js");
|
| 75 |
+
std::fs::write(&scriptlets_path, &scriptlets_js).map_err(|e| e.to_string())?;
|
| 76 |
+
resources = parse_scriptlet_file(&scriptlets_path);
|
| 77 |
+
println!("[muse-shield] Upstream uBO scriptlets parsed {} resources", resources.len());
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
Err(e) => eprintln!("[muse-shield] Failed to fetch upstream uBO scriptlets: {e}"),
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// 2) Deterministic fallback bundle in adblock-rust legacy resource format.
|
| 84 |
+
if resources.is_empty() {
|
| 85 |
+
let fallback_path = cache_dir.join("muse-fallback-scriptlets.js");
|
| 86 |
+
std::fs::write(&fallback_path, FALLBACK_SCRIPTLETS).map_err(|e| e.to_string())?;
|
| 87 |
+
resources = parse_scriptlet_file(&fallback_path);
|
| 88 |
+
println!("[muse-shield] Fallback scriptlets parsed {} resources", resources.len());
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if !resources.is_empty() {
|
| 92 |
+
let resource_count = resources.len();
|
| 93 |
+
new_engine.use_resources(resources);
|
| 94 |
+
println!("[muse-shield] Loaded {resource_count} scriptlet resources via adblock-rust resource_assembler");
|
| 95 |
+
} else {
|
| 96 |
+
eprintln!("[muse-shield] WARNING: scriptlet resource parsing returned 0 even for fallback bundle. Built-in early video scriptlets remain active.");
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
let state = app.state::<AdBlockState>();
|
| 100 |
+
state.reload_engine(new_engine, total_rules);
|
| 101 |
+
println!("[muse-shield] Engine reloaded with {total_rules} rules + scriptlet resources");
|
| 102 |
+
Ok(())
|
| 103 |
+
}
|
src-tauri/src/board.rs
CHANGED
|
@@ -1,222 +1,222 @@
|
|
| 1 |
-
use base64::{engine::general_purpose, Engine as _};
|
| 2 |
-
use serde::{Deserialize, Serialize};
|
| 3 |
-
use std::sync::Mutex;
|
| 4 |
-
use tauri::{AppHandle, Manager};
|
| 5 |
-
use uuid::Uuid;
|
| 6 |
-
|
| 7 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 8 |
-
pub struct BoardItem {
|
| 9 |
-
pub id: String,
|
| 10 |
-
pub kind: String,
|
| 11 |
-
pub library_id: Option<String>,
|
| 12 |
-
pub data_url: Option<String>,
|
| 13 |
-
pub text: Option<String>,
|
| 14 |
-
pub colors: Vec<String>,
|
| 15 |
-
pub x: f64,
|
| 16 |
-
pub y: f64,
|
| 17 |
-
pub w: f64,
|
| 18 |
-
pub h: f64,
|
| 19 |
-
pub z: i64,
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 23 |
-
pub struct BoardSummary {
|
| 24 |
-
pub id: String,
|
| 25 |
-
pub title: String,
|
| 26 |
-
pub item_count: usize,
|
| 27 |
-
pub created_at: i64,
|
| 28 |
-
pub updated_at: i64,
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 32 |
-
pub struct BoardDocument {
|
| 33 |
-
pub id: String,
|
| 34 |
-
pub title: String,
|
| 35 |
-
pub items: Vec<BoardItem>,
|
| 36 |
-
pub created_at: i64,
|
| 37 |
-
pub updated_at: i64,
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
impl Default for BoardDocument {
|
| 41 |
-
fn default() -> Self {
|
| 42 |
-
let now = chrono::Utc::now().timestamp();
|
| 43 |
-
Self { id: "default".into(), title: "Reference Board".into(), items: vec![], created_at: now, updated_at: now }
|
| 44 |
-
}
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
| 48 |
-
struct BoardIndex { boards: Vec<BoardSummary>, active_id: Option<String> }
|
| 49 |
-
|
| 50 |
-
#[derive(Default)]
|
| 51 |
-
pub struct BoardState { pub current: Mutex<Option<BoardDocument>> }
|
| 52 |
-
|
| 53 |
-
fn board_file(id: &str) -> String { format!("board_{id}.json") }
|
| 54 |
-
fn save_index(app: &AppHandle, index: &BoardIndex) -> Result<(), String> { crate::persistence::save_json(app, "boards_index.json", index) }
|
| 55 |
-
fn load_index(app: &AppHandle) -> Result<BoardIndex, String> { crate::persistence::load_json(app, "boards_index.json") }
|
| 56 |
-
fn save_doc(app: &AppHandle, doc: &BoardDocument) -> Result<(), String> { crate::persistence::save_json(app, &board_file(&doc.id), doc) }
|
| 57 |
-
fn load_doc(app: &AppHandle, id: &str) -> Result<BoardDocument, String> { crate::persistence::load_json(app, &board_file(id)) }
|
| 58 |
-
|
| 59 |
-
fn update_index_for_doc(app: &AppHandle, doc: &BoardDocument, make_active: bool) -> Result<(), String> {
|
| 60 |
-
let mut index = load_index(app).unwrap_or_default();
|
| 61 |
-
let summary = BoardSummary { id: doc.id.clone(), title: doc.title.clone(), item_count: doc.items.len(), created_at: doc.created_at, updated_at: doc.updated_at };
|
| 62 |
-
if let Some(slot) = index.boards.iter_mut().find(|b| b.id == doc.id) { *slot = summary; } else { index.boards.insert(0, summary); }
|
| 63 |
-
if make_active { index.active_id = Some(doc.id.clone()); }
|
| 64 |
-
save_index(app, &index)
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
fn ensure_current(app: &AppHandle) -> Result<BoardDocument, String> {
|
| 68 |
-
let state = app.state::<BoardState>();
|
| 69 |
-
if let Some(doc) = state.current.lock().map_err(|_| "board lock poisoned".to_string())?.clone() { return Ok(doc); }
|
| 70 |
-
let index = load_index(app).unwrap_or_default();
|
| 71 |
-
if let Some(active) = index.active_id.clone().or_else(|| index.boards.first().map(|b| b.id.clone())) {
|
| 72 |
-
let doc = load_doc(app, &active).unwrap_or_default();
|
| 73 |
-
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 74 |
-
return Ok(doc);
|
| 75 |
-
}
|
| 76 |
-
let doc = BoardDocument::default();
|
| 77 |
-
save_doc(app, &doc)?;
|
| 78 |
-
let mut idx = load_index(app).unwrap_or_default();
|
| 79 |
-
idx.boards.push(BoardSummary { id: doc.id.clone(), title: doc.title.clone(), item_count: 0, created_at: doc.created_at, updated_at: doc.updated_at });
|
| 80 |
-
idx.active_id = Some(doc.id.clone());
|
| 81 |
-
save_index(app, &idx)?;
|
| 82 |
-
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 83 |
-
Ok(doc)
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
fn save_current(app: &AppHandle, mut doc: BoardDocument) -> Result<BoardDocument, String> {
|
| 87 |
-
doc.updated_at = chrono::Utc::now().timestamp();
|
| 88 |
-
save_doc(app, &doc)?;
|
| 89 |
-
update_index_for_doc(app, &doc, true)?;
|
| 90 |
-
let state = app.state::<BoardState>();
|
| 91 |
-
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 92 |
-
Ok(doc)
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
#[tauri::command]
|
| 96 |
-
pub fn board_list(app: AppHandle) -> Result<Vec<BoardSummary>, String> { Ok(load_index(&app).unwrap_or_default().boards) }
|
| 97 |
-
#[tauri::command]
|
| 98 |
-
pub fn board_current(app: AppHandle) -> Result<BoardDocument, String> { ensure_current(&app) }
|
| 99 |
-
|
| 100 |
-
#[tauri::command]
|
| 101 |
-
pub fn board_create(app: AppHandle, title: String) -> Result<BoardDocument, String> {
|
| 102 |
-
let now = chrono::Utc::now().timestamp();
|
| 103 |
-
let doc = BoardDocument { id: Uuid::new_v4().to_string(), title: if title.trim().is_empty() { "Untitled Board".into() } else { title }, items: vec![], created_at: now, updated_at: now };
|
| 104 |
-
save_doc(&app, &doc)?; update_index_for_doc(&app, &doc, true)?;
|
| 105 |
-
let state = app.state::<BoardState>();
|
| 106 |
-
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 107 |
-
Ok(doc)
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
#[tauri::command]
|
| 111 |
-
pub fn board_open(app: AppHandle, id: String) -> Result<BoardDocument, String> {
|
| 112 |
-
let doc = load_doc(&app, &id)?; update_index_for_doc(&app, &doc, true)?;
|
| 113 |
-
let state = app.state::<BoardState>();
|
| 114 |
-
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 115 |
-
Ok(doc)
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
#[tauri::command]
|
| 119 |
-
pub fn board_save_as(app: AppHandle, title: String) -> Result<BoardDocument, String> {
|
| 120 |
-
let current = ensure_current(&app)?;
|
| 121 |
-
let now = chrono::Utc::now().timestamp();
|
| 122 |
-
let doc = BoardDocument { id: Uuid::new_v4().to_string(), title: if title.trim().is_empty() { format!("{} copy", current.title) } else { title }, items: current.items, created_at: now, updated_at: now };
|
| 123 |
-
save_doc(&app, &doc)?; update_index_for_doc(&app, &doc, true)?;
|
| 124 |
-
let state = app.state::<BoardState>();
|
| 125 |
-
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 126 |
-
Ok(doc)
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
#[tauri::command]
|
| 130 |
-
pub fn board_load(app: AppHandle) -> Result<Vec<BoardItem>, String> { Ok(ensure_current(&app)?.items) }
|
| 131 |
-
#[tauri::command]
|
| 132 |
-
pub fn board_items(app: AppHandle) -> Result<Vec<BoardItem>, String> { board_load(app) }
|
| 133 |
-
|
| 134 |
-
#[tauri::command]
|
| 135 |
-
pub fn board_add_image(app: AppHandle, library_id: Option<String>, data_url: String, x: f64, y: f64, w: f64, h: f64) -> Result<BoardItem, String> {
|
| 136 |
-
let mut doc = ensure_current(&app)?;
|
| 137 |
-
let item = BoardItem { id: Uuid::new_v4().to_string(), kind: "image".into(), library_id, data_url: Some(data_url), text: None, colors: vec![], x, y, w, h, z: chrono::Utc::now().timestamp_millis() };
|
| 138 |
-
doc.items.push(item.clone()); save_current(&app, doc)?; Ok(item)
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
#[tauri::command]
|
| 142 |
-
pub fn board_add_note(app: AppHandle, text: String, x: f64, y: f64) -> Result<BoardItem, String> {
|
| 143 |
-
let mut doc = ensure_current(&app)?;
|
| 144 |
-
let item = BoardItem { id: Uuid::new_v4().to_string(), kind: "note".into(), library_id: None, data_url: None, text: Some(text), colors: vec![], x, y, w: 240.0, h: 160.0, z: chrono::Utc::now().timestamp_millis() };
|
| 145 |
-
doc.items.push(item.clone()); save_current(&app, doc)?; Ok(item)
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
#[tauri::command]
|
| 149 |
-
pub fn board_add_palette(app: AppHandle, colors: Vec<String>, x: f64, y: f64) -> Result<BoardItem, String> {
|
| 150 |
-
if colors.is_empty() { return Err("no colors to add".into()); }
|
| 151 |
-
let mut doc = ensure_current(&app)?;
|
| 152 |
-
let item = BoardItem { id: Uuid::new_v4().to_string(), kind: "palette".into(), library_id: None, data_url: None, text: None, colors, x, y, w: 260.0, h: 90.0, z: chrono::Utc::now().timestamp_millis() };
|
| 153 |
-
doc.items.push(item.clone()); save_current(&app, doc)?; Ok(item)
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
#[tauri::command]
|
| 157 |
-
pub async fn board_extract_palette_from_item(app: AppHandle, id: String, count: Option<usize>) -> Result<Vec<String>, String> {
|
| 158 |
-
let mut doc = ensure_current(&app)?;
|
| 159 |
-
let item = doc.items.iter_mut().find(|i| i.id == id).ok_or("board item not found")?;
|
| 160 |
-
let src = item.data_url.clone().ok_or("board item has no image source")?;
|
| 161 |
-
let bytes = image_bytes_from_source(&src).await?;
|
| 162 |
-
let img = image::load_from_memory(&bytes).map_err(|e| format!("unsupported image: {e}"))?;
|
| 163 |
-
let colors = extract_palette(&img, count.unwrap_or(6).clamp(1, 12));
|
| 164 |
-
item.colors = colors.clone();
|
| 165 |
-
save_current(&app, doc)?; Ok(colors)
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
#[tauri::command]
|
| 169 |
-
pub async fn board_add_palette_from_item(app: AppHandle, id: String, x: f64, y: f64) -> Result<BoardItem, String> {
|
| 170 |
-
let colors = board_extract_palette_from_item(app.clone(), id, Some(6)).await?;
|
| 171 |
-
board_add_palette(app, colors, x, y)
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
#[tauri::command]
|
| 175 |
-
pub fn board_update_item(app: AppHandle, item: BoardItem) -> Result<(), String> {
|
| 176 |
-
let mut doc = ensure_current(&app)?;
|
| 177 |
-
if let Some(slot) = doc.items.iter_mut().find(|i| i.id == item.id) { *slot = item; }
|
| 178 |
-
save_current(&app, doc).map(|_| ())
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
#[tauri::command]
|
| 182 |
-
pub fn board_delete_item(app: AppHandle, id: String) -> Result<(), String> {
|
| 183 |
-
let mut doc = ensure_current(&app)?;
|
| 184 |
-
doc.items.retain(|i| i.id != id);
|
| 185 |
-
save_current(&app, doc).map(|_| ())
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
async fn image_bytes_from_source(src: &str) -> Result<Vec<u8>, String> {
|
| 189 |
-
if let Some((_, encoded)) = src.split_once(",") {
|
| 190 |
-
if src.starts_with("data:image/") { return general_purpose::STANDARD.decode(encoded).map_err(|e| e.to_string()); }
|
| 191 |
-
}
|
| 192 |
-
if src.starts_with("http://") || src.starts_with("https://") {
|
| 193 |
-
return Ok(reqwest::get(src).await.map_err(|e| e.to_string())?.bytes().await.map_err(|e| e.to_string())?.to_vec());
|
| 194 |
-
}
|
| 195 |
-
Err("unsupported image source".into())
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
fn extract_palette(img: &image::DynamicImage, count: usize) -> Vec<String> {
|
| 199 |
-
let thumb = img.thumbnail(120, 120).to_rgb8();
|
| 200 |
-
let mut pixels: Vec<[u8; 3]> = thumb.pixels().filter_map(|p| {
|
| 201 |
-
let [r, g, b] = [p[0], p[1], p[2]];
|
| 202 |
-
if r.max(g).max(b) < 12 { None } else { Some([r, g, b]) }
|
| 203 |
-
}).collect();
|
| 204 |
-
if pixels.is_empty() { pixels = thumb.pixels().map(|p| [p[0], p[1], p[2]]).collect(); }
|
| 205 |
-
let mut buckets = vec![pixels];
|
| 206 |
-
while buckets.len() < count {
|
| 207 |
-
let Some((idx, axis)) = buckets.iter().enumerate().filter(|(_, b)| b.len() > 1).map(|(i, b)| {
|
| 208 |
-
let ranges: Vec<u8> = (0..3).map(|c| b.iter().map(|p| p[c]).max().unwrap_or(0).saturating_sub(b.iter().map(|p| p[c]).min().unwrap_or(0))).collect();
|
| 209 |
-
let axis = ranges.iter().enumerate().max_by_key(|(_, r)| *r).map(|(a, _)| a).unwrap_or(0);
|
| 210 |
-
(i, axis)
|
| 211 |
-
}).max_by_key(|(i, axis)| {
|
| 212 |
-
let b = &buckets[*i];
|
| 213 |
-
b.iter().map(|p| p[*axis]).max().unwrap_or(0).saturating_sub(b.iter().map(|p| p[*axis]).min().unwrap_or(0))
|
| 214 |
-
}) else { break; };
|
| 215 |
-
let mut b = buckets.remove(idx); b.sort_by_key(|p| p[axis]); let right = b.split_off(b.len() / 2); buckets.push(b); buckets.push(right);
|
| 216 |
-
}
|
| 217 |
-
buckets.into_iter().filter(|b| !b.is_empty()).map(|b| {
|
| 218 |
-
let n = b.len() as u32;
|
| 219 |
-
let (r, g, bl) = b.iter().fold((0u32, 0u32, 0u32), |a, p| (a.0 + p[0] as u32, a.1 + p[1] as u32, a.2 + p[2] as u32));
|
| 220 |
-
format!("#{:02X}{:02X}{:02X}", r / n, g / n, bl / n)
|
| 221 |
-
}).collect()
|
| 222 |
-
}
|
|
|
|
| 1 |
+
use base64::{engine::general_purpose, Engine as _};
|
| 2 |
+
use serde::{Deserialize, Serialize};
|
| 3 |
+
use std::sync::Mutex;
|
| 4 |
+
use tauri::{AppHandle, Manager};
|
| 5 |
+
use uuid::Uuid;
|
| 6 |
+
|
| 7 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 8 |
+
pub struct BoardItem {
|
| 9 |
+
pub id: String,
|
| 10 |
+
pub kind: String,
|
| 11 |
+
pub library_id: Option<String>,
|
| 12 |
+
pub data_url: Option<String>,
|
| 13 |
+
pub text: Option<String>,
|
| 14 |
+
pub colors: Vec<String>,
|
| 15 |
+
pub x: f64,
|
| 16 |
+
pub y: f64,
|
| 17 |
+
pub w: f64,
|
| 18 |
+
pub h: f64,
|
| 19 |
+
pub z: i64,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 23 |
+
pub struct BoardSummary {
|
| 24 |
+
pub id: String,
|
| 25 |
+
pub title: String,
|
| 26 |
+
pub item_count: usize,
|
| 27 |
+
pub created_at: i64,
|
| 28 |
+
pub updated_at: i64,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 32 |
+
pub struct BoardDocument {
|
| 33 |
+
pub id: String,
|
| 34 |
+
pub title: String,
|
| 35 |
+
pub items: Vec<BoardItem>,
|
| 36 |
+
pub created_at: i64,
|
| 37 |
+
pub updated_at: i64,
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
impl Default for BoardDocument {
|
| 41 |
+
fn default() -> Self {
|
| 42 |
+
let now = chrono::Utc::now().timestamp();
|
| 43 |
+
Self { id: "default".into(), title: "Reference Board".into(), items: vec![], created_at: now, updated_at: now }
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
| 48 |
+
struct BoardIndex { boards: Vec<BoardSummary>, active_id: Option<String> }
|
| 49 |
+
|
| 50 |
+
#[derive(Default)]
|
| 51 |
+
pub struct BoardState { pub current: Mutex<Option<BoardDocument>> }
|
| 52 |
+
|
| 53 |
+
fn board_file(id: &str) -> String { format!("board_{id}.json") }
|
| 54 |
+
fn save_index(app: &AppHandle, index: &BoardIndex) -> Result<(), String> { crate::persistence::save_json(app, "boards_index.json", index) }
|
| 55 |
+
fn load_index(app: &AppHandle) -> Result<BoardIndex, String> { crate::persistence::load_json(app, "boards_index.json") }
|
| 56 |
+
fn save_doc(app: &AppHandle, doc: &BoardDocument) -> Result<(), String> { crate::persistence::save_json(app, &board_file(&doc.id), doc) }
|
| 57 |
+
fn load_doc(app: &AppHandle, id: &str) -> Result<BoardDocument, String> { crate::persistence::load_json(app, &board_file(id)) }
|
| 58 |
+
|
| 59 |
+
fn update_index_for_doc(app: &AppHandle, doc: &BoardDocument, make_active: bool) -> Result<(), String> {
|
| 60 |
+
let mut index = load_index(app).unwrap_or_default();
|
| 61 |
+
let summary = BoardSummary { id: doc.id.clone(), title: doc.title.clone(), item_count: doc.items.len(), created_at: doc.created_at, updated_at: doc.updated_at };
|
| 62 |
+
if let Some(slot) = index.boards.iter_mut().find(|b| b.id == doc.id) { *slot = summary; } else { index.boards.insert(0, summary); }
|
| 63 |
+
if make_active { index.active_id = Some(doc.id.clone()); }
|
| 64 |
+
save_index(app, &index)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
fn ensure_current(app: &AppHandle) -> Result<BoardDocument, String> {
|
| 68 |
+
let state = app.state::<BoardState>();
|
| 69 |
+
if let Some(doc) = state.current.lock().map_err(|_| "board lock poisoned".to_string())?.clone() { return Ok(doc); }
|
| 70 |
+
let index = load_index(app).unwrap_or_default();
|
| 71 |
+
if let Some(active) = index.active_id.clone().or_else(|| index.boards.first().map(|b| b.id.clone())) {
|
| 72 |
+
let doc = load_doc(app, &active).unwrap_or_default();
|
| 73 |
+
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 74 |
+
return Ok(doc);
|
| 75 |
+
}
|
| 76 |
+
let doc = BoardDocument::default();
|
| 77 |
+
save_doc(app, &doc)?;
|
| 78 |
+
let mut idx = load_index(app).unwrap_or_default();
|
| 79 |
+
idx.boards.push(BoardSummary { id: doc.id.clone(), title: doc.title.clone(), item_count: 0, created_at: doc.created_at, updated_at: doc.updated_at });
|
| 80 |
+
idx.active_id = Some(doc.id.clone());
|
| 81 |
+
save_index(app, &idx)?;
|
| 82 |
+
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 83 |
+
Ok(doc)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
fn save_current(app: &AppHandle, mut doc: BoardDocument) -> Result<BoardDocument, String> {
|
| 87 |
+
doc.updated_at = chrono::Utc::now().timestamp();
|
| 88 |
+
save_doc(app, &doc)?;
|
| 89 |
+
update_index_for_doc(app, &doc, true)?;
|
| 90 |
+
let state = app.state::<BoardState>();
|
| 91 |
+
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 92 |
+
Ok(doc)
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
#[tauri::command]
|
| 96 |
+
pub fn board_list(app: AppHandle) -> Result<Vec<BoardSummary>, String> { Ok(load_index(&app).unwrap_or_default().boards) }
|
| 97 |
+
#[tauri::command]
|
| 98 |
+
pub fn board_current(app: AppHandle) -> Result<BoardDocument, String> { ensure_current(&app) }
|
| 99 |
+
|
| 100 |
+
#[tauri::command]
|
| 101 |
+
pub fn board_create(app: AppHandle, title: String) -> Result<BoardDocument, String> {
|
| 102 |
+
let now = chrono::Utc::now().timestamp();
|
| 103 |
+
let doc = BoardDocument { id: Uuid::new_v4().to_string(), title: if title.trim().is_empty() { "Untitled Board".into() } else { title }, items: vec![], created_at: now, updated_at: now };
|
| 104 |
+
save_doc(&app, &doc)?; update_index_for_doc(&app, &doc, true)?;
|
| 105 |
+
let state = app.state::<BoardState>();
|
| 106 |
+
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 107 |
+
Ok(doc)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
#[tauri::command]
|
| 111 |
+
pub fn board_open(app: AppHandle, id: String) -> Result<BoardDocument, String> {
|
| 112 |
+
let doc = load_doc(&app, &id)?; update_index_for_doc(&app, &doc, true)?;
|
| 113 |
+
let state = app.state::<BoardState>();
|
| 114 |
+
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 115 |
+
Ok(doc)
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
#[tauri::command]
|
| 119 |
+
pub fn board_save_as(app: AppHandle, title: String) -> Result<BoardDocument, String> {
|
| 120 |
+
let current = ensure_current(&app)?;
|
| 121 |
+
let now = chrono::Utc::now().timestamp();
|
| 122 |
+
let doc = BoardDocument { id: Uuid::new_v4().to_string(), title: if title.trim().is_empty() { format!("{} copy", current.title) } else { title }, items: current.items, created_at: now, updated_at: now };
|
| 123 |
+
save_doc(&app, &doc)?; update_index_for_doc(&app, &doc, true)?;
|
| 124 |
+
let state = app.state::<BoardState>();
|
| 125 |
+
*state.current.lock().map_err(|_| "board lock poisoned".to_string())? = Some(doc.clone());
|
| 126 |
+
Ok(doc)
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
#[tauri::command]
|
| 130 |
+
pub fn board_load(app: AppHandle) -> Result<Vec<BoardItem>, String> { Ok(ensure_current(&app)?.items) }
|
| 131 |
+
#[tauri::command]
|
| 132 |
+
pub fn board_items(app: AppHandle) -> Result<Vec<BoardItem>, String> { board_load(app) }
|
| 133 |
+
|
| 134 |
+
#[tauri::command]
|
| 135 |
+
pub fn board_add_image(app: AppHandle, library_id: Option<String>, data_url: String, x: f64, y: f64, w: f64, h: f64) -> Result<BoardItem, String> {
|
| 136 |
+
let mut doc = ensure_current(&app)?;
|
| 137 |
+
let item = BoardItem { id: Uuid::new_v4().to_string(), kind: "image".into(), library_id, data_url: Some(data_url), text: None, colors: vec![], x, y, w, h, z: chrono::Utc::now().timestamp_millis() };
|
| 138 |
+
doc.items.push(item.clone()); save_current(&app, doc)?; Ok(item)
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
#[tauri::command]
|
| 142 |
+
pub fn board_add_note(app: AppHandle, text: String, x: f64, y: f64) -> Result<BoardItem, String> {
|
| 143 |
+
let mut doc = ensure_current(&app)?;
|
| 144 |
+
let item = BoardItem { id: Uuid::new_v4().to_string(), kind: "note".into(), library_id: None, data_url: None, text: Some(text), colors: vec![], x, y, w: 240.0, h: 160.0, z: chrono::Utc::now().timestamp_millis() };
|
| 145 |
+
doc.items.push(item.clone()); save_current(&app, doc)?; Ok(item)
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
#[tauri::command]
|
| 149 |
+
pub fn board_add_palette(app: AppHandle, colors: Vec<String>, x: f64, y: f64) -> Result<BoardItem, String> {
|
| 150 |
+
if colors.is_empty() { return Err("no colors to add".into()); }
|
| 151 |
+
let mut doc = ensure_current(&app)?;
|
| 152 |
+
let item = BoardItem { id: Uuid::new_v4().to_string(), kind: "palette".into(), library_id: None, data_url: None, text: None, colors, x, y, w: 260.0, h: 90.0, z: chrono::Utc::now().timestamp_millis() };
|
| 153 |
+
doc.items.push(item.clone()); save_current(&app, doc)?; Ok(item)
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
#[tauri::command]
|
| 157 |
+
pub async fn board_extract_palette_from_item(app: AppHandle, id: String, count: Option<usize>) -> Result<Vec<String>, String> {
|
| 158 |
+
let mut doc = ensure_current(&app)?;
|
| 159 |
+
let item = doc.items.iter_mut().find(|i| i.id == id).ok_or("board item not found")?;
|
| 160 |
+
let src = item.data_url.clone().ok_or("board item has no image source")?;
|
| 161 |
+
let bytes = image_bytes_from_source(&src).await?;
|
| 162 |
+
let img = image::load_from_memory(&bytes).map_err(|e| format!("unsupported image: {e}"))?;
|
| 163 |
+
let colors = extract_palette(&img, count.unwrap_or(6).clamp(1, 12));
|
| 164 |
+
item.colors = colors.clone();
|
| 165 |
+
save_current(&app, doc)?; Ok(colors)
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
#[tauri::command]
|
| 169 |
+
pub async fn board_add_palette_from_item(app: AppHandle, id: String, x: f64, y: f64) -> Result<BoardItem, String> {
|
| 170 |
+
let colors = board_extract_palette_from_item(app.clone(), id, Some(6)).await?;
|
| 171 |
+
board_add_palette(app, colors, x, y)
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
#[tauri::command]
|
| 175 |
+
pub fn board_update_item(app: AppHandle, item: BoardItem) -> Result<(), String> {
|
| 176 |
+
let mut doc = ensure_current(&app)?;
|
| 177 |
+
if let Some(slot) = doc.items.iter_mut().find(|i| i.id == item.id) { *slot = item; }
|
| 178 |
+
save_current(&app, doc).map(|_| ())
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
#[tauri::command]
|
| 182 |
+
pub fn board_delete_item(app: AppHandle, id: String) -> Result<(), String> {
|
| 183 |
+
let mut doc = ensure_current(&app)?;
|
| 184 |
+
doc.items.retain(|i| i.id != id);
|
| 185 |
+
save_current(&app, doc).map(|_| ())
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
async fn image_bytes_from_source(src: &str) -> Result<Vec<u8>, String> {
|
| 189 |
+
if let Some((_, encoded)) = src.split_once(",") {
|
| 190 |
+
if src.starts_with("data:image/") { return general_purpose::STANDARD.decode(encoded).map_err(|e| e.to_string()); }
|
| 191 |
+
}
|
| 192 |
+
if src.starts_with("http://") || src.starts_with("https://") {
|
| 193 |
+
return Ok(reqwest::get(src).await.map_err(|e| e.to_string())?.bytes().await.map_err(|e| e.to_string())?.to_vec());
|
| 194 |
+
}
|
| 195 |
+
Err("unsupported image source".into())
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
fn extract_palette(img: &image::DynamicImage, count: usize) -> Vec<String> {
|
| 199 |
+
let thumb = img.thumbnail(120, 120).to_rgb8();
|
| 200 |
+
let mut pixels: Vec<[u8; 3]> = thumb.pixels().filter_map(|p| {
|
| 201 |
+
let [r, g, b] = [p[0], p[1], p[2]];
|
| 202 |
+
if r.max(g).max(b) < 12 { None } else { Some([r, g, b]) }
|
| 203 |
+
}).collect();
|
| 204 |
+
if pixels.is_empty() { pixels = thumb.pixels().map(|p| [p[0], p[1], p[2]]).collect(); }
|
| 205 |
+
let mut buckets = vec![pixels];
|
| 206 |
+
while buckets.len() < count {
|
| 207 |
+
let Some((idx, axis)) = buckets.iter().enumerate().filter(|(_, b)| b.len() > 1).map(|(i, b)| {
|
| 208 |
+
let ranges: Vec<u8> = (0..3).map(|c| b.iter().map(|p| p[c]).max().unwrap_or(0).saturating_sub(b.iter().map(|p| p[c]).min().unwrap_or(0))).collect();
|
| 209 |
+
let axis = ranges.iter().enumerate().max_by_key(|(_, r)| *r).map(|(a, _)| a).unwrap_or(0);
|
| 210 |
+
(i, axis)
|
| 211 |
+
}).max_by_key(|(i, axis)| {
|
| 212 |
+
let b = &buckets[*i];
|
| 213 |
+
b.iter().map(|p| p[*axis]).max().unwrap_or(0).saturating_sub(b.iter().map(|p| p[*axis]).min().unwrap_or(0))
|
| 214 |
+
}) else { break; };
|
| 215 |
+
let mut b = buckets.remove(idx); b.sort_by_key(|p| p[axis]); let right = b.split_off(b.len() / 2); buckets.push(b); buckets.push(right);
|
| 216 |
+
}
|
| 217 |
+
buckets.into_iter().filter(|b| !b.is_empty()).map(|b| {
|
| 218 |
+
let n = b.len() as u32;
|
| 219 |
+
let (r, g, bl) = b.iter().fold((0u32, 0u32, 0u32), |a, p| (a.0 + p[0] as u32, a.1 + p[1] as u32, a.2 + p[2] as u32));
|
| 220 |
+
format!("#{:02X}{:02X}{:02X}", r / n, g / n, bl / n)
|
| 221 |
+
}).collect()
|
| 222 |
+
}
|
src-tauri/src/browser/autofill.rs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
use tauri::AppHandle;
|
| 2 |
-
use super::tab_manager::eval_on_tab;
|
| 3 |
-
|
| 4 |
-
/// Injects saved credentials into the active child webview's login form.
|
| 5 |
-
/// Called by the React frontend after user selects a credential from the vault.
|
| 6 |
-
/// Uses the __museAutofill() function that vault_detector.js defines in every page.
|
| 7 |
-
#[tauri::command]
|
| 8 |
-
pub fn tab_autofill(app: AppHandle, tab_id: String, username: String, password: String) -> Result<(), String> {
|
| 9 |
-
let username_js = serde_json::to_string(&username).map_err(|e| e.to_string())?;
|
| 10 |
-
let password_js = serde_json::to_string(&password).map_err(|e| e.to_string())?;
|
| 11 |
-
let js = format!("window.__museAutofill && window.__museAutofill({username_js}, {password_js})");
|
| 12 |
-
eval_on_tab(&app, &tab_id, &js)
|
| 13 |
-
}
|
|
|
|
| 1 |
+
use tauri::AppHandle;
|
| 2 |
+
use super::tab_manager::eval_on_tab;
|
| 3 |
+
|
| 4 |
+
/// Injects saved credentials into the active child webview's login form.
|
| 5 |
+
/// Called by the React frontend after user selects a credential from the vault.
|
| 6 |
+
/// Uses the __museAutofill() function that vault_detector.js defines in every page.
|
| 7 |
+
#[tauri::command]
|
| 8 |
+
pub fn tab_autofill(app: AppHandle, tab_id: String, username: String, password: String) -> Result<(), String> {
|
| 9 |
+
let username_js = serde_json::to_string(&username).map_err(|e| e.to_string())?;
|
| 10 |
+
let password_js = serde_json::to_string(&password).map_err(|e| e.to_string())?;
|
| 11 |
+
let js = format!("window.__museAutofill && window.__museAutofill({username_js}, {password_js})");
|
| 12 |
+
eval_on_tab(&app, &tab_id, &js)
|
| 13 |
+
}
|
src-tauri/src/browser/capture.rs
CHANGED
|
@@ -1,93 +1,93 @@
|
|
| 1 |
-
use serde::{Deserialize, Serialize};
|
| 2 |
-
use tauri::{AppHandle, Manager};
|
| 3 |
-
|
| 4 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 5 |
-
#[serde(rename_all = "camelCase")]
|
| 6 |
-
pub struct CaptureClip {
|
| 7 |
-
pub x: f64,
|
| 8 |
-
pub y: f64,
|
| 9 |
-
pub width: f64,
|
| 10 |
-
pub height: f64,
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 14 |
-
#[serde(rename_all = "camelCase")]
|
| 15 |
-
pub struct ContentLayout {
|
| 16 |
-
pub x: f64,
|
| 17 |
-
pub y: f64,
|
| 18 |
-
pub width: f64,
|
| 19 |
-
pub height: f64,
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
#[tauri::command]
|
| 23 |
-
pub fn browser_capture_viewport(app: AppHandle, layout: ContentLayout) -> Result<String, String> {
|
| 24 |
-
if layout.width < 10.0 || layout.height < 10.0 {
|
| 25 |
-
return Err("content area is too small to capture".into());
|
| 26 |
-
}
|
| 27 |
-
let window = app.get_window("main").ok_or("main window not found")?;
|
| 28 |
-
let scale = window.scale_factor().map_err(|e| format!("scale_factor: {e}"))?;
|
| 29 |
-
let outer = window.outer_position().map_err(|e| format!("outer_position: {e}"))?;
|
| 30 |
-
let abs_x = outer.x + (layout.x * scale).round() as i32;
|
| 31 |
-
let abs_y = outer.y + (layout.y * scale).round() as i32;
|
| 32 |
-
let abs_w = (layout.width * scale).round().max(1.0) as u32;
|
| 33 |
-
let abs_h = (layout.height * scale).round().max(1.0) as u32;
|
| 34 |
-
capture_screen_region(abs_x, abs_y, abs_w, abs_h)
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
#[tauri::command]
|
| 38 |
-
pub fn browser_capture_clip(app: AppHandle, layout: ContentLayout, clip: CaptureClip) -> Result<String, String> {
|
| 39 |
-
if clip.width < 1.0 || clip.height < 1.0 {
|
| 40 |
-
return Err("clip area is too small".into());
|
| 41 |
-
}
|
| 42 |
-
if layout.width < 10.0 || layout.height < 10.0 {
|
| 43 |
-
return Err("content area is too small".into());
|
| 44 |
-
}
|
| 45 |
-
let window = app.get_window("main").ok_or("main window not found")?;
|
| 46 |
-
let scale = window.scale_factor().map_err(|e| format!("scale_factor: {e}"))?;
|
| 47 |
-
let outer = window.outer_position().map_err(|e| format!("outer_position: {e}"))?;
|
| 48 |
-
let abs_x = outer.x + ((layout.x + clip.x) * scale).round() as i32;
|
| 49 |
-
let abs_y = outer.y + ((layout.y + clip.y) * scale).round() as i32;
|
| 50 |
-
let abs_w = (clip.width * scale).round().max(1.0) as u32;
|
| 51 |
-
let abs_h = (clip.height * scale).round().max(1.0) as u32;
|
| 52 |
-
capture_screen_region(abs_x, abs_y, abs_w, abs_h)
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
#[tauri::command]
|
| 56 |
-
pub fn browser_capture_full_page(app: AppHandle, layout: ContentLayout) -> Result<String, String> {
|
| 57 |
-
browser_capture_viewport(app, layout)
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
fn capture_screen_region(x: i32, y: i32, width: u32, height: u32) -> Result<String, String> {
|
| 61 |
-
use base64::{engine::general_purpose, Engine as _};
|
| 62 |
-
use screenshots::Screen;
|
| 63 |
-
|
| 64 |
-
if width < 1 || height < 1 {
|
| 65 |
-
return Err("capture region is empty".into());
|
| 66 |
-
}
|
| 67 |
-
let screens = Screen::all().map_err(|e| format!("failed to enumerate screens: {e}"))?;
|
| 68 |
-
let cx = x + (width as i32 / 2);
|
| 69 |
-
let cy = y + (height as i32 / 2);
|
| 70 |
-
let screen = screens.iter().find(|s| {
|
| 71 |
-
let di = s.display_info;
|
| 72 |
-
let sx = di.x as i32;
|
| 73 |
-
let sy = di.y as i32;
|
| 74 |
-
let sw = di.width as i32;
|
| 75 |
-
let sh = di.height as i32;
|
| 76 |
-
cx >= sx && cy >= sy && cx < sx + sw && cy < sy + sh
|
| 77 |
-
}).or_else(|| screens.first()).ok_or("no screen found")?;
|
| 78 |
-
|
| 79 |
-
let capture = screen.capture_area(x, y, width, height)
|
| 80 |
-
.map_err(|e| format!("screen capture failed (x={x}, y={y}, w={width}, h={height}): {e}"))?;
|
| 81 |
-
let cw = capture.width();
|
| 82 |
-
let ch = capture.height();
|
| 83 |
-
let raw = capture.into_raw();
|
| 84 |
-
|
| 85 |
-
let img_buf = image::RgbaImage::from_raw(cw, ch, raw)
|
| 86 |
-
.ok_or_else(|| "failed to create image buffer".to_string())?;
|
| 87 |
-
let dynamic = image::DynamicImage::ImageRgba8(img_buf);
|
| 88 |
-
let mut png_bytes: Vec<u8> = Vec::new();
|
| 89 |
-
dynamic.write_to(&mut std::io::Cursor::new(&mut png_bytes), image::ImageFormat::Png)
|
| 90 |
-
.map_err(|e| format!("PNG encode failed: {e}"))?;
|
| 91 |
-
let b64 = general_purpose::STANDARD.encode(&png_bytes);
|
| 92 |
-
Ok(format!("data:image/png;base64,{b64}"))
|
| 93 |
-
}
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use tauri::{AppHandle, Manager};
|
| 3 |
+
|
| 4 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 5 |
+
#[serde(rename_all = "camelCase")]
|
| 6 |
+
pub struct CaptureClip {
|
| 7 |
+
pub x: f64,
|
| 8 |
+
pub y: f64,
|
| 9 |
+
pub width: f64,
|
| 10 |
+
pub height: f64,
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 14 |
+
#[serde(rename_all = "camelCase")]
|
| 15 |
+
pub struct ContentLayout {
|
| 16 |
+
pub x: f64,
|
| 17 |
+
pub y: f64,
|
| 18 |
+
pub width: f64,
|
| 19 |
+
pub height: f64,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
#[tauri::command]
|
| 23 |
+
pub fn browser_capture_viewport(app: AppHandle, layout: ContentLayout) -> Result<String, String> {
|
| 24 |
+
if layout.width < 10.0 || layout.height < 10.0 {
|
| 25 |
+
return Err("content area is too small to capture".into());
|
| 26 |
+
}
|
| 27 |
+
let window = app.get_window("main").ok_or("main window not found")?;
|
| 28 |
+
let scale = window.scale_factor().map_err(|e| format!("scale_factor: {e}"))?;
|
| 29 |
+
let outer = window.outer_position().map_err(|e| format!("outer_position: {e}"))?;
|
| 30 |
+
let abs_x = outer.x + (layout.x * scale).round() as i32;
|
| 31 |
+
let abs_y = outer.y + (layout.y * scale).round() as i32;
|
| 32 |
+
let abs_w = (layout.width * scale).round().max(1.0) as u32;
|
| 33 |
+
let abs_h = (layout.height * scale).round().max(1.0) as u32;
|
| 34 |
+
capture_screen_region(abs_x, abs_y, abs_w, abs_h)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#[tauri::command]
|
| 38 |
+
pub fn browser_capture_clip(app: AppHandle, layout: ContentLayout, clip: CaptureClip) -> Result<String, String> {
|
| 39 |
+
if clip.width < 1.0 || clip.height < 1.0 {
|
| 40 |
+
return Err("clip area is too small".into());
|
| 41 |
+
}
|
| 42 |
+
if layout.width < 10.0 || layout.height < 10.0 {
|
| 43 |
+
return Err("content area is too small".into());
|
| 44 |
+
}
|
| 45 |
+
let window = app.get_window("main").ok_or("main window not found")?;
|
| 46 |
+
let scale = window.scale_factor().map_err(|e| format!("scale_factor: {e}"))?;
|
| 47 |
+
let outer = window.outer_position().map_err(|e| format!("outer_position: {e}"))?;
|
| 48 |
+
let abs_x = outer.x + ((layout.x + clip.x) * scale).round() as i32;
|
| 49 |
+
let abs_y = outer.y + ((layout.y + clip.y) * scale).round() as i32;
|
| 50 |
+
let abs_w = (clip.width * scale).round().max(1.0) as u32;
|
| 51 |
+
let abs_h = (clip.height * scale).round().max(1.0) as u32;
|
| 52 |
+
capture_screen_region(abs_x, abs_y, abs_w, abs_h)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
#[tauri::command]
|
| 56 |
+
pub fn browser_capture_full_page(app: AppHandle, layout: ContentLayout) -> Result<String, String> {
|
| 57 |
+
browser_capture_viewport(app, layout)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
fn capture_screen_region(x: i32, y: i32, width: u32, height: u32) -> Result<String, String> {
|
| 61 |
+
use base64::{engine::general_purpose, Engine as _};
|
| 62 |
+
use screenshots::Screen;
|
| 63 |
+
|
| 64 |
+
if width < 1 || height < 1 {
|
| 65 |
+
return Err("capture region is empty".into());
|
| 66 |
+
}
|
| 67 |
+
let screens = Screen::all().map_err(|e| format!("failed to enumerate screens: {e}"))?;
|
| 68 |
+
let cx = x + (width as i32 / 2);
|
| 69 |
+
let cy = y + (height as i32 / 2);
|
| 70 |
+
let screen = screens.iter().find(|s| {
|
| 71 |
+
let di = s.display_info;
|
| 72 |
+
let sx = di.x as i32;
|
| 73 |
+
let sy = di.y as i32;
|
| 74 |
+
let sw = di.width as i32;
|
| 75 |
+
let sh = di.height as i32;
|
| 76 |
+
cx >= sx && cy >= sy && cx < sx + sw && cy < sy + sh
|
| 77 |
+
}).or_else(|| screens.first()).ok_or("no screen found")?;
|
| 78 |
+
|
| 79 |
+
let capture = screen.capture_area(x, y, width, height)
|
| 80 |
+
.map_err(|e| format!("screen capture failed (x={x}, y={y}, w={width}, h={height}): {e}"))?;
|
| 81 |
+
let cw = capture.width();
|
| 82 |
+
let ch = capture.height();
|
| 83 |
+
let raw = capture.into_raw();
|
| 84 |
+
|
| 85 |
+
let img_buf = image::RgbaImage::from_raw(cw, ch, raw)
|
| 86 |
+
.ok_or_else(|| "failed to create image buffer".to_string())?;
|
| 87 |
+
let dynamic = image::DynamicImage::ImageRgba8(img_buf);
|
| 88 |
+
let mut png_bytes: Vec<u8> = Vec::new();
|
| 89 |
+
dynamic.write_to(&mut std::io::Cursor::new(&mut png_bytes), image::ImageFormat::Png)
|
| 90 |
+
.map_err(|e| format!("PNG encode failed: {e}"))?;
|
| 91 |
+
let b64 = general_purpose::STANDARD.encode(&png_bytes);
|
| 92 |
+
Ok(format!("data:image/png;base64,{b64}"))
|
| 93 |
+
}
|
src-tauri/src/browser/commands.rs
CHANGED
|
@@ -1,225 +1,272 @@
|
|
| 1 |
-
use std::collections::HashMap;
|
| 2 |
-
use tauri::webview::{PageLoadEvent, WebviewBuilder};
|
| 3 |
-
use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Url, WebviewUrl};
|
| 4 |
-
|
| 5 |
-
use super::layout::{bounds, hide_tab, resize_active, show_tab};
|
| 6 |
-
use super::navigation::{navigation_blocked, resolve_url};
|
| 7 |
-
use super::tab_manager::*;
|
| 8 |
-
use crate::adblock::engine::AdBlockState;
|
| 9 |
-
use crate::adblock::scripts;
|
| 10 |
-
use crate::state::AppState;
|
| 11 |
-
|
| 12 |
-
const CONTEXT_MENU_BLOCK_JS: &str = r#"
|
| 13 |
-
(function(){
|
| 14 |
-
if(window.__muse_ctx_installed) return;
|
| 15 |
-
window.__muse_ctx_installed = true;
|
| 16 |
-
document.addEventListener('contextmenu', function(e) {
|
| 17 |
-
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
| 18 |
-
try {
|
| 19 |
-
var t = e.target;
|
| 20 |
-
window.__TAURI_INTERNALS__.invoke('browser_context_menu', {
|
| 21 |
-
x: e.screenX, y: e.screenY, clientX: e.clientX, clientY: e.clientY,
|
| 22 |
-
tagName: t.tagName || null, src: t.src || t.currentSrc || null,
|
| 23 |
-
href: t.closest && t.closest('a') ? t.closest('a').href : null,
|
| 24 |
-
text: (window.getSelection() || '').toString().slice(0, 200) || null,
|
| 25 |
-
pageUrl: location.href
|
| 26 |
-
});
|
| 27 |
-
} catch(ex) {}
|
| 28 |
-
return false;
|
| 29 |
-
}, true);
|
| 30 |
-
window.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, true);
|
| 31 |
-
})();
|
| 32 |
-
"#;
|
| 33 |
-
|
| 34 |
-
#[tauri::command]
|
| 35 |
-
pub async fn browser_init(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 36 |
-
let existing = snapshot(&app)?;
|
| 37 |
-
if !existing.tabs.is_empty() {
|
| 38 |
-
if layout.x > 0.0 || layout.y > 0.0 { resize_active(&app, &layout)?; }
|
| 39 |
-
return Ok(existing);
|
| 40 |
-
}
|
| 41 |
-
create_tab_inner(&app, "https://duckduckgo.com", &layout, false).await?;
|
| 42 |
-
snapshot(&app)
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
#[tauri::command]
|
| 46 |
-
pub async fn browser_set_visible(app: AppHandle, visible: bool, layout: ViewportLayout) -> Result<(), String> {
|
| 47 |
-
let active = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.active.clone() };
|
| 48 |
-
if let Some(active_id) = active {
|
| 49 |
-
if visible && layout.width > 10.0 && layout.height > 10.0 { show_tab(&app, &active_id, &layout)?; } else { hide_tab(&app, &active_id)?; }
|
| 50 |
-
}
|
| 51 |
-
Ok(())
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
#[tauri::command]
|
| 55 |
-
pub async fn tab_create(app: AppHandle, url: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 56 |
-
let resolved = resolve_url(&url);
|
| 57 |
-
if navigation_blocked(&app, &resolved) { return snapshot(&app); }
|
| 58 |
-
create_tab_inner(&app, &resolved, &layout, false).await?;
|
| 59 |
-
snapshot(&app)
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
#[tauri::command]
|
| 63 |
-
pub async fn tab_activate(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 64 |
-
let previous = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let previous = tabs.active.clone(); if !tabs.tabs.contains_key(&tab_id) { return Err(format!("tab not found: {tab_id}")); } tabs.active = Some(tab_id.clone()); if let Some(tab) = tabs.tabs.get_mut(&tab_id) { tab.last_active = chrono::Utc::now().timestamp(); } previous };
|
| 65 |
-
if let Some(prev) = previous { if prev != tab_id { hide_tab(&app, &prev)?; } }
|
| 66 |
-
if layout.width > 10.0 && layout.height > 10.0 && layout.x > 0.0 { show_tab(&app, &tab_id, &layout)?; }
|
| 67 |
-
emit_snapshot(&app)?; snapshot(&app)
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
#[tauri::command]
|
| 71 |
-
pub async fn tab_close(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 72 |
-
let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let label = tabs.tabs.get(&tab_id).map(|t| t.label.clone()); if let Some(tab) = tabs.tabs.remove(&tab_id) { tabs.push_closed(tab); } tabs.order.retain(|id| id != &tab_id); if tabs.active.as_deref() == Some(&tab_id) { tabs.active = tabs.order.last().cloned(); } label };
|
| 73 |
-
if let Some(label) = label { if let Some(webview) = app.get_webview(&label) { let _ = webview.close(); } }
|
| 74 |
-
let active = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.active.clone() };
|
| 75 |
-
if let Some(active_id) = active { if layout.width > 10.0 && layout.height > 10.0 && layout.x > 0.0 { show_tab(&app, &active_id, &layout)?; } }
|
| 76 |
-
emit_snapshot(&app)?; snapshot(&app)
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
#[tauri::command]
|
| 80 |
-
pub async fn tab_restore(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 81 |
-
let url = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.pop_closed().map(|t| t.url) };
|
| 82 |
-
if let Some(url) = url { create_tab_inner(&app, &url, &layout, false).await?; }
|
| 83 |
-
snapshot(&app)
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
#[tauri::command]
|
| 87 |
-
pub async fn tab_navigate(app: AppHandle, tab_id: String, url: String) -> Result<BrowserSnapshot, String> {
|
| 88 |
-
let resolved = resolve_url(&url);
|
| 89 |
-
if navigation_blocked(&app, &resolved) { return snapshot(&app); }
|
| 90 |
-
let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.url = resolved.clone(); tab.loading = true; tab.can_go_back = true; tab.label.clone() };
|
| 91 |
-
let webview = app.get_webview(&label).ok_or("webview not found")?;
|
| 92 |
-
let parsed = Url::parse(&resolved).map_err(|e| e.to_string())?;
|
| 93 |
-
webview.navigate(parsed).map_err(|e| e.to_string())?;
|
| 94 |
-
emit_snapshot(&app)?; snapshot(&app)
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
#[tauri::command]
|
| 98 |
-
pub async fn tab_reload(app: AppHandle, tab_id: String) -> Result<(), String> { let label = tab_label(&app, &tab_id)?; app.get_webview(&label).ok_or("webview not found")?.reload().map_err(|e| e.to_string()) }
|
| 99 |
-
#[tauri::command]
|
| 100 |
-
pub async fn tab_back(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.back()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_forward = true; }); Ok(()) }
|
| 101 |
-
#[tauri::command]
|
| 102 |
-
pub async fn tab_forward(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.forward()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_back = true; }); Ok(()) }
|
| 103 |
-
|
| 104 |
-
#[tauri::command]
|
| 105 |
-
pub async fn tab_zoom(app: AppHandle, tab_id: String, zoom: f64) -> Result<BrowserSnapshot, String> {
|
| 106 |
-
let zoom = zoom.clamp(0.5, 2.0);
|
| 107 |
-
let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let (label, domain) = { let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.zoom = zoom; let domain = extract_domain(&tab.url); (tab.label.clone(), domain) }; tabs.remember_zoom(&domain, zoom); label };
|
| 108 |
-
app.get_webview(&label).ok_or("webview not found")?.set_zoom(zoom).map_err(|e| e.to_string())?;
|
| 109 |
-
let state = app.state::<AppState>(); if let Ok(tabs) = state.tabs.lock() { let _ = crate::persistence::save_json(&app, "zoom_memory.json", &tabs.zoom_memory); }
|
| 110 |
-
snapshot(&app)
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
#[tauri::command]
|
| 114 |
-
pub async fn tab_resize(app: AppHandle, layout: ViewportLayout) -> Result<(), String> { resize_active(&app, &layout) }
|
| 115 |
-
#[tauri::command]
|
| 116 |
-
pub fn tab_get_all(app: AppHandle) -> Result<BrowserSnapshot, String> { snapshot(&app) }
|
| 117 |
-
#[tauri::command]
|
| 118 |
-
pub fn tab_pin(app: AppHandle, tab_id: String, pinned: bool) -> Result<BrowserSnapshot, String> { update_tab_field(&app, &tab_id, |t| { t.pinned = pinned; }); snapshot(&app) }
|
| 119 |
-
|
| 120 |
-
#[tauri::command]
|
| 121 |
-
pub async fn tab_find(app: AppHandle, tab_id: String, query: String) -> Result<u32, String> {
|
| 122 |
-
let q = serde_json::to_string(&query).unwrap_or("\"\"".to_string());
|
| 123 |
-
let js = format!(r#"(function(){{window.__muse_find_cleanup&&window.__muse_find_cleanup();if(!{q})return 0;const text={q};const walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let count=0;const marks=[];while(walker.nextNode()){{const node=walker.currentNode;const idx=node.textContent.toLowerCase().indexOf(text.toLowerCase());if(idx>=0){{const range=document.createRange();range.setStart(node,idx);range.setEnd(node,idx+text.length);const mark=document.createElement('mark');mark.style.cssText='background:#C49A3C;color:#100E0B;border-radius:2px;padding:0 1px';range.surroundContents(mark);marks.push(mark);count++;}}}}if(marks.length>0)marks[0].scrollIntoView({{block:'center'}});window.__muse_find_cleanup=()=>marks.forEach(m=>{{const p=m.parentNode;if(p){{p.replaceChild(document.createTextNode(m.textContent||''),m);p.normalize();}}}});return count;}})()"#);
|
| 124 |
-
eval_on_tab(&app, &tab_id, &js)?; Ok(0)
|
| 125 |
-
}
|
| 126 |
-
#[tauri::command]
|
| 127 |
-
pub async fn tab_find_clear(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "window.__muse_find_cleanup && window.__muse_find_cleanup()") }
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
let
|
| 135 |
-
let
|
| 136 |
-
|
| 137 |
-
let
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
let
|
| 141 |
-
let
|
| 142 |
-
|
| 143 |
-
let
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
});
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
let
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::collections::HashMap;
|
| 2 |
+
use tauri::webview::{PageLoadEvent, WebviewBuilder};
|
| 3 |
+
use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Url, WebviewUrl};
|
| 4 |
+
|
| 5 |
+
use super::layout::{bounds, hide_tab, resize_active, show_tab};
|
| 6 |
+
use super::navigation::{navigation_blocked, resolve_url};
|
| 7 |
+
use super::tab_manager::*;
|
| 8 |
+
use crate::adblock::engine::AdBlockState;
|
| 9 |
+
use crate::adblock::scripts;
|
| 10 |
+
use crate::state::AppState;
|
| 11 |
+
|
| 12 |
+
const CONTEXT_MENU_BLOCK_JS: &str = r#"
|
| 13 |
+
(function(){
|
| 14 |
+
if(window.__muse_ctx_installed) return;
|
| 15 |
+
window.__muse_ctx_installed = true;
|
| 16 |
+
document.addEventListener('contextmenu', function(e) {
|
| 17 |
+
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
| 18 |
+
try {
|
| 19 |
+
var t = e.target;
|
| 20 |
+
window.__TAURI_INTERNALS__.invoke('browser_context_menu', {
|
| 21 |
+
x: e.screenX, y: e.screenY, clientX: e.clientX, clientY: e.clientY,
|
| 22 |
+
tagName: t.tagName || null, src: t.src || t.currentSrc || null,
|
| 23 |
+
href: t.closest && t.closest('a') ? t.closest('a').href : null,
|
| 24 |
+
text: (window.getSelection() || '').toString().slice(0, 200) || null,
|
| 25 |
+
pageUrl: location.href
|
| 26 |
+
});
|
| 27 |
+
} catch(ex) {}
|
| 28 |
+
return false;
|
| 29 |
+
}, true);
|
| 30 |
+
window.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, true);
|
| 31 |
+
})();
|
| 32 |
+
"#;
|
| 33 |
+
|
| 34 |
+
#[tauri::command]
|
| 35 |
+
pub async fn browser_init(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 36 |
+
let existing = snapshot(&app)?;
|
| 37 |
+
if !existing.tabs.is_empty() {
|
| 38 |
+
if layout.x > 0.0 || layout.y > 0.0 { resize_active(&app, &layout)?; }
|
| 39 |
+
return Ok(existing);
|
| 40 |
+
}
|
| 41 |
+
create_tab_inner(&app, "https://duckduckgo.com", &layout, false).await?;
|
| 42 |
+
snapshot(&app)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
#[tauri::command]
|
| 46 |
+
pub async fn browser_set_visible(app: AppHandle, visible: bool, layout: ViewportLayout) -> Result<(), String> {
|
| 47 |
+
let active = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.active.clone() };
|
| 48 |
+
if let Some(active_id) = active {
|
| 49 |
+
if visible && layout.width > 10.0 && layout.height > 10.0 { show_tab(&app, &active_id, &layout)?; } else { hide_tab(&app, &active_id)?; }
|
| 50 |
+
}
|
| 51 |
+
Ok(())
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
#[tauri::command]
|
| 55 |
+
pub async fn tab_create(app: AppHandle, url: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 56 |
+
let resolved = resolve_url(&url);
|
| 57 |
+
if navigation_blocked(&app, &resolved) { return snapshot(&app); }
|
| 58 |
+
create_tab_inner(&app, &resolved, &layout, false).await?;
|
| 59 |
+
snapshot(&app)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
#[tauri::command]
|
| 63 |
+
pub async fn tab_activate(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 64 |
+
let previous = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let previous = tabs.active.clone(); if !tabs.tabs.contains_key(&tab_id) { return Err(format!("tab not found: {tab_id}")); } tabs.active = Some(tab_id.clone()); if let Some(tab) = tabs.tabs.get_mut(&tab_id) { tab.last_active = chrono::Utc::now().timestamp(); } previous };
|
| 65 |
+
if let Some(prev) = previous { if prev != tab_id { hide_tab(&app, &prev)?; } }
|
| 66 |
+
if layout.width > 10.0 && layout.height > 10.0 && layout.x > 0.0 { show_tab(&app, &tab_id, &layout)?; }
|
| 67 |
+
emit_snapshot(&app)?; snapshot(&app)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
#[tauri::command]
|
| 71 |
+
pub async fn tab_close(app: AppHandle, tab_id: String, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 72 |
+
let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let label = tabs.tabs.get(&tab_id).map(|t| t.label.clone()); if let Some(tab) = tabs.tabs.remove(&tab_id) { tabs.push_closed(tab); } tabs.order.retain(|id| id != &tab_id); if tabs.active.as_deref() == Some(&tab_id) { tabs.active = tabs.order.last().cloned(); } label };
|
| 73 |
+
if let Some(label) = label { if let Some(webview) = app.get_webview(&label) { let _ = webview.close(); } }
|
| 74 |
+
let active = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.active.clone() };
|
| 75 |
+
if let Some(active_id) = active { if layout.width > 10.0 && layout.height > 10.0 && layout.x > 0.0 { show_tab(&app, &active_id, &layout)?; } }
|
| 76 |
+
emit_snapshot(&app)?; snapshot(&app)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
#[tauri::command]
|
| 80 |
+
pub async fn tab_restore(app: AppHandle, layout: ViewportLayout) -> Result<BrowserSnapshot, String> {
|
| 81 |
+
let url = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.pop_closed().map(|t| t.url) };
|
| 82 |
+
if let Some(url) = url { create_tab_inner(&app, &url, &layout, false).await?; }
|
| 83 |
+
snapshot(&app)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
#[tauri::command]
|
| 87 |
+
pub async fn tab_navigate(app: AppHandle, tab_id: String, url: String) -> Result<BrowserSnapshot, String> {
|
| 88 |
+
let resolved = resolve_url(&url);
|
| 89 |
+
if navigation_blocked(&app, &resolved) { return snapshot(&app); }
|
| 90 |
+
let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.url = resolved.clone(); tab.loading = true; tab.can_go_back = true; tab.label.clone() };
|
| 91 |
+
let webview = app.get_webview(&label).ok_or("webview not found")?;
|
| 92 |
+
let parsed = Url::parse(&resolved).map_err(|e| e.to_string())?;
|
| 93 |
+
webview.navigate(parsed).map_err(|e| e.to_string())?;
|
| 94 |
+
emit_snapshot(&app)?; snapshot(&app)
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
#[tauri::command]
|
| 98 |
+
pub async fn tab_reload(app: AppHandle, tab_id: String) -> Result<(), String> { let label = tab_label(&app, &tab_id)?; app.get_webview(&label).ok_or("webview not found")?.reload().map_err(|e| e.to_string()) }
|
| 99 |
+
#[tauri::command]
|
| 100 |
+
pub async fn tab_back(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.back()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_forward = true; }); Ok(()) }
|
| 101 |
+
#[tauri::command]
|
| 102 |
+
pub async fn tab_forward(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "history.forward()")?; update_tab_field(&app, &tab_id, |t| { t.can_go_back = true; }); Ok(()) }
|
| 103 |
+
|
| 104 |
+
#[tauri::command]
|
| 105 |
+
pub async fn tab_zoom(app: AppHandle, tab_id: String, zoom: f64) -> Result<BrowserSnapshot, String> {
|
| 106 |
+
let zoom = zoom.clamp(0.5, 2.0);
|
| 107 |
+
let label = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let (label, domain) = { let tab = tabs.tabs.get_mut(&tab_id).ok_or("tab not found")?; tab.zoom = zoom; let domain = extract_domain(&tab.url); (tab.label.clone(), domain) }; tabs.remember_zoom(&domain, zoom); label };
|
| 108 |
+
app.get_webview(&label).ok_or("webview not found")?.set_zoom(zoom).map_err(|e| e.to_string())?;
|
| 109 |
+
let state = app.state::<AppState>(); if let Ok(tabs) = state.tabs.lock() { let _ = crate::persistence::save_json(&app, "zoom_memory.json", &tabs.zoom_memory); }
|
| 110 |
+
snapshot(&app)
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
#[tauri::command]
|
| 114 |
+
pub async fn tab_resize(app: AppHandle, layout: ViewportLayout) -> Result<(), String> { resize_active(&app, &layout) }
|
| 115 |
+
#[tauri::command]
|
| 116 |
+
pub fn tab_get_all(app: AppHandle) -> Result<BrowserSnapshot, String> { snapshot(&app) }
|
| 117 |
+
#[tauri::command]
|
| 118 |
+
pub fn tab_pin(app: AppHandle, tab_id: String, pinned: bool) -> Result<BrowserSnapshot, String> { update_tab_field(&app, &tab_id, |t| { t.pinned = pinned; }); snapshot(&app) }
|
| 119 |
+
|
| 120 |
+
#[tauri::command]
|
| 121 |
+
pub async fn tab_find(app: AppHandle, tab_id: String, query: String) -> Result<u32, String> {
|
| 122 |
+
let q = serde_json::to_string(&query).unwrap_or("\"\"".to_string());
|
| 123 |
+
let js = format!(r#"(function(){{window.__muse_find_cleanup&&window.__muse_find_cleanup();if(!{q})return 0;const text={q};const walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let count=0;const marks=[];while(walker.nextNode()){{const node=walker.currentNode;const idx=node.textContent.toLowerCase().indexOf(text.toLowerCase());if(idx>=0){{const range=document.createRange();range.setStart(node,idx);range.setEnd(node,idx+text.length);const mark=document.createElement('mark');mark.style.cssText='background:#C49A3C;color:#100E0B;border-radius:2px;padding:0 1px';range.surroundContents(mark);marks.push(mark);count++;}}}}if(marks.length>0)marks[0].scrollIntoView({{block:'center'}});window.__muse_find_cleanup=()=>marks.forEach(m=>{{const p=m.parentNode;if(p){{p.replaceChild(document.createTextNode(m.textContent||''),m);p.normalize();}}}});return count;}})()"#);
|
| 124 |
+
eval_on_tab(&app, &tab_id, &js)?; Ok(0)
|
| 125 |
+
}
|
| 126 |
+
#[tauri::command]
|
| 127 |
+
pub async fn tab_find_clear(app: AppHandle, tab_id: String) -> Result<(), String> { eval_on_tab(&app, &tab_id, "window.__muse_find_cleanup && window.__muse_find_cleanup()") }
|
| 128 |
+
|
| 129 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 130 |
+
// INTERNAL: Child webview creation
|
| 131 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 132 |
+
|
| 133 |
+
pub(crate) async fn create_tab_inner(app: &AppHandle, url: &str, layout: &ViewportLayout, show_immediately: bool) -> Result<String, String> {
|
| 134 |
+
let id_num = app.state::<AppState>().next_tab_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
| 135 |
+
let id = format!("tab-{id_num}");
|
| 136 |
+
let label = format!("muse-tab-{id_num}");
|
| 137 |
+
let parsed = Url::parse(url).map_err(|e| e.to_string())?;
|
| 138 |
+
|
| 139 |
+
// Build the full initialization script (adblock + privacy + hover overlay + vault detector + autofill suppression)
|
| 140 |
+
let init_script = scripts::build_init_script(&scripts::blocked_domains_json());
|
| 141 |
+
let full_init = format!("{init_script}\n{CONTEXT_MENU_BLOCK_JS}\n{FAVICON_SCRIPT}");
|
| 142 |
+
|
| 143 |
+
let app_for_load = app.clone();
|
| 144 |
+
let id_for_load = id.clone();
|
| 145 |
+
let app_for_title = app.clone();
|
| 146 |
+
let id_for_title = id.clone();
|
| 147 |
+
let app_for_cosmetic = app.clone();
|
| 148 |
+
|
| 149 |
+
let builder = WebviewBuilder::new(label.clone(), WebviewUrl::External(parsed))
|
| 150 |
+
// Inject all scripts before any page JS runs
|
| 151 |
+
.initialization_script(&full_init)
|
| 152 |
+
// Disable native WebView autofill (Windows WebView2: general autofill suggestions)
|
| 153 |
+
// macOS/Linux: this is a noop — autofill suppression handled by JS injection instead
|
| 154 |
+
.general_autofill_enabled(false)
|
| 155 |
+
// Page load handler: re-inject scripts, apply adblock cosmetics, record history
|
| 156 |
+
.on_page_load(move |webview, payload| {
|
| 157 |
+
let url = payload.url().to_string();
|
| 158 |
+
let loading = matches!(payload.event(), PageLoadEvent::Started);
|
| 159 |
+
update_tab_field(&app_for_load, &id_for_load, |t| { t.url = url.clone(); t.loading = loading; });
|
| 160 |
+
|
| 161 |
+
if matches!(payload.event(), PageLoadEvent::Finished) {
|
| 162 |
+
// Re-inject scripts that may not persist across cross-origin navigations
|
| 163 |
+
let _ = webview.eval(CONTEXT_MENU_BLOCK_JS);
|
| 164 |
+
let _ = webview.eval(scripts::hover_overlay_script());
|
| 165 |
+
let _ = webview.eval(scripts::vault_detector_script());
|
| 166 |
+
let _ = webview.eval(scripts::autofill_suppress_script());
|
| 167 |
+
|
| 168 |
+
// Adblock cosmetic filtering + scriptlets
|
| 169 |
+
let adblock_state = app_for_cosmetic.state::<AdBlockState>();
|
| 170 |
+
let css = adblock_state.get_cosmetic_css(&url);
|
| 171 |
+
if !css.is_empty() {
|
| 172 |
+
let escaped = css.replace('\\', "\\\\").replace('`', "\\`");
|
| 173 |
+
let _ = webview.eval(&format!("(function(){{const s=document.createElement('style');s.id='__muse_shield';s.textContent=`{escaped}`;document.head.appendChild(s)}})();"));
|
| 174 |
+
}
|
| 175 |
+
let scriptlet_js = adblock_state.get_injected_script(&url);
|
| 176 |
+
if !scriptlet_js.is_empty() { let _ = webview.eval(&format!("try{{{scriptlet_js}}}catch(e){{}}")); }
|
| 177 |
+
|
| 178 |
+
// Favicon extraction
|
| 179 |
+
let _ = webview.eval("window.__muse_report_favicon && window.__muse_report_favicon()");
|
| 180 |
+
|
| 181 |
+
// Record page visit in app-managed history
|
| 182 |
+
let title_for_history = {
|
| 183 |
+
let state = app_for_load.state::<AppState>();
|
| 184 |
+
state.tabs.lock().ok().and_then(|tabs| tabs.tabs.get(&id_for_load).map(|t| t.title.clone())).unwrap_or_default()
|
| 185 |
+
};
|
| 186 |
+
let _ = crate::history::record_visit(&app_for_load, id_for_load.clone(), url.clone(), title_for_history);
|
| 187 |
+
}
|
| 188 |
+
})
|
| 189 |
+
// Title change handler
|
| 190 |
+
.on_document_title_changed(move |_webview, title| {
|
| 191 |
+
update_tab_field(&app_for_title, &id_for_title, |t| { if !title.trim().is_empty() { t.title = title.clone(); } });
|
| 192 |
+
})
|
| 193 |
+
// Navigation handler: intercept muse-action:// beacons, block ad domains
|
| 194 |
+
.on_navigation({ let app_nav = app.clone(); move |url| {
|
| 195 |
+
let s = url.as_str();
|
| 196 |
+
if s.starts_with("muse-action://") { handle_muse_action(&app_nav, s); return false; }
|
| 197 |
+
!navigation_blocked(&app_nav, s)
|
| 198 |
+
}});
|
| 199 |
+
|
| 200 |
+
// Add child webview to the main window
|
| 201 |
+
let window = app.get_window("main").ok_or("main window not found")?;
|
| 202 |
+
let (cx, cy, cw, ch) = if show_immediately && layout.width > 10.0 { bounds(layout) } else { (-32000.0, -32000.0, 1.0, 1.0) };
|
| 203 |
+
window.add_child(builder, LogicalPosition::new(cx, cy), LogicalSize::new(cw, ch)).map_err(|e| e.to_string())?;
|
| 204 |
+
if !show_immediately { if let Some(webview) = app.get_webview(&label) { let _ = webview.hide(); } }
|
| 205 |
+
|
| 206 |
+
// Apply per-domain zoom memory
|
| 207 |
+
let domain = extract_domain(url);
|
| 208 |
+
let saved_zoom = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.get_zoom_for_domain(&domain) };
|
| 209 |
+
let now = chrono::Utc::now().timestamp();
|
| 210 |
+
let zoom = saved_zoom.unwrap_or(1.0);
|
| 211 |
+
|
| 212 |
+
// Register tab in state and activate
|
| 213 |
+
let previous = { let state = app.state::<AppState>(); let mut tabs = state.tabs.lock().map_err(|_| "lock")?; let previous = tabs.active.clone(); tabs.active = Some(id.clone()); tabs.order.push(id.clone()); tabs.tabs.insert(id.clone(), BrowserTab { id: id.clone(), label: label.clone(), url: url.to_string(), title: "New Tab".to_string(), favicon: None, loading: true, pinned: false, sleeping: false, zoom, can_go_back: false, can_go_forward: false, last_active: now }); previous };
|
| 214 |
+
if zoom != 1.0 { if let Some(webview) = app.get_webview(&label) { let _ = webview.set_zoom(zoom); } }
|
| 215 |
+
if let Some(prev) = previous { if prev != id { hide_tab(app, &prev)?; } }
|
| 216 |
+
emit_snapshot(app)?;
|
| 217 |
+
Ok(id)
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 221 |
+
// muse-action:// beacon handler (hover overlay, vault detector)
|
| 222 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 223 |
+
|
| 224 |
+
fn handle_muse_action(app: &AppHandle, raw: &str) {
|
| 225 |
+
let rest = raw.trim_start_matches("muse-action://");
|
| 226 |
+
let (_action, query) = rest.split_once('?').unwrap_or((rest, ""));
|
| 227 |
+
let params = parse_query(query);
|
| 228 |
+
let url = params.get("url").cloned().unwrap_or_default();
|
| 229 |
+
if url.is_empty() { return; }
|
| 230 |
+
let title = params.get("title").cloned();
|
| 231 |
+
let source = params.get("source").cloned();
|
| 232 |
+
|
| 233 |
+
let app2 = app.clone();
|
| 234 |
+
tauri::async_runtime::spawn(async move {
|
| 235 |
+
match crate::library::library_add_item(app2.clone(), url.clone(), source, title).await {
|
| 236 |
+
Ok(item) => {
|
| 237 |
+
let _ = crate::board::board_add_image(app2.clone(), Some(item.id.clone()), item.data_url.clone(), 100.0 + (chrono::Utc::now().timestamp_millis() % 200) as f64, 100.0 + (chrono::Utc::now().timestamp_millis() % 150) as f64, 300.0, 200.0);
|
| 238 |
+
let _ = app2.emit("board://image_added", serde_json::json!({"id": item.id, "url": item.data_url, "source_url": item.url, "width": item.width, "height": item.height}));
|
| 239 |
+
}
|
| 240 |
+
Err(e) => eprintln!("[muse-action] Failed to capture image: {e}"),
|
| 241 |
+
}
|
| 242 |
+
});
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 246 |
+
// Utility functions
|
| 247 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 248 |
+
|
| 249 |
+
fn parse_query(query: &str) -> HashMap<String, String> {
|
| 250 |
+
query.split('&').filter_map(|pair| {
|
| 251 |
+
let (k, v) = pair.split_once('=')?;
|
| 252 |
+
Some((percent_decode(k), percent_decode(v)))
|
| 253 |
+
}).collect()
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
fn percent_decode(s: &str) -> String {
|
| 257 |
+
let bytes = s.as_bytes();
|
| 258 |
+
let mut out = Vec::with_capacity(bytes.len());
|
| 259 |
+
let mut i = 0;
|
| 260 |
+
while i < bytes.len() {
|
| 261 |
+
if bytes[i] == b'%' && i + 2 < bytes.len() {
|
| 262 |
+
if let Ok(hex) = std::str::from_utf8(&bytes[i + 1..i + 3]) {
|
| 263 |
+
if let Ok(v) = u8::from_str_radix(hex, 16) { out.push(v); i += 3; continue; }
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
out.push(if bytes[i] == b'+' { b' ' } else { bytes[i] });
|
| 267 |
+
i += 1;
|
| 268 |
+
}
|
| 269 |
+
String::from_utf8_lossy(&out).to_string()
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
const FAVICON_SCRIPT: &str = r#"(function(){window.__muse_report_favicon=function(){const link=document.querySelector('link[rel~="icon"],link[rel="shortcut icon"],link[rel="apple-touch-icon"]');if(link&&link.href){try{window.__TAURI_INTERNALS__.invoke('__tab_favicon',{favicon:link.href});}catch{}}};if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',window.__muse_report_favicon);}else{window.__muse_report_favicon();}})();"#;
|
src-tauri/src/browser/context_menu.rs
CHANGED
|
@@ -1,23 +1,23 @@
|
|
| 1 |
-
use serde::{Deserialize, Serialize};
|
| 2 |
-
use tauri::{AppHandle, Emitter};
|
| 3 |
-
|
| 4 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 5 |
-
#[serde(rename_all = "camelCase")]
|
| 6 |
-
pub struct ContextMenuInfo {
|
| 7 |
-
pub x: f64,
|
| 8 |
-
pub y: f64,
|
| 9 |
-
pub client_x: f64,
|
| 10 |
-
pub client_y: f64,
|
| 11 |
-
pub tag_name: Option<String>,
|
| 12 |
-
pub src: Option<String>,
|
| 13 |
-
pub href: Option<String>,
|
| 14 |
-
pub text: Option<String>,
|
| 15 |
-
pub page_url: Option<String>,
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
/// Called from the child WebView's contextmenu event handler.
|
| 19 |
-
/// Emits the context menu info to the React shell which renders its own menu.
|
| 20 |
-
#[tauri::command]
|
| 21 |
-
pub fn browser_context_menu(app: AppHandle, info: ContextMenuInfo) -> Result<(), String> {
|
| 22 |
-
app.emit("browser://context-menu", info).map_err(|e| e.to_string())
|
| 23 |
-
}
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use tauri::{AppHandle, Emitter};
|
| 3 |
+
|
| 4 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 5 |
+
#[serde(rename_all = "camelCase")]
|
| 6 |
+
pub struct ContextMenuInfo {
|
| 7 |
+
pub x: f64,
|
| 8 |
+
pub y: f64,
|
| 9 |
+
pub client_x: f64,
|
| 10 |
+
pub client_y: f64,
|
| 11 |
+
pub tag_name: Option<String>,
|
| 12 |
+
pub src: Option<String>,
|
| 13 |
+
pub href: Option<String>,
|
| 14 |
+
pub text: Option<String>,
|
| 15 |
+
pub page_url: Option<String>,
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/// Called from the child WebView's contextmenu event handler.
|
| 19 |
+
/// Emits the context menu info to the React shell which renders its own menu.
|
| 20 |
+
#[tauri::command]
|
| 21 |
+
pub fn browser_context_menu(app: AppHandle, info: ContextMenuInfo) -> Result<(), String> {
|
| 22 |
+
app.emit("browser://context-menu", info).map_err(|e| e.to_string())
|
| 23 |
+
}
|
src-tauri/src/browser/layout.rs
CHANGED
|
@@ -1,49 +1,49 @@
|
|
| 1 |
-
use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager};
|
| 2 |
-
|
| 3 |
-
use super::tab_manager::{tab_label, ViewportLayout};
|
| 4 |
-
use crate::state::AppState;
|
| 5 |
-
|
| 6 |
-
const HIDDEN_X: f64 = -32000.0;
|
| 7 |
-
const HIDDEN_Y: f64 = -32000.0;
|
| 8 |
-
|
| 9 |
-
/// Calculate child WebView bounds from measured React host rect.
|
| 10 |
-
/// React sends getBoundingClientRect() values in logical/CSS pixels.
|
| 11 |
-
pub fn bounds(layout: &ViewportLayout) -> (f64, f64, f64, f64) {
|
| 12 |
-
let x = layout.x.max(0.0);
|
| 13 |
-
let y = layout.y.max(0.0);
|
| 14 |
-
let w = layout.width.max(1.0);
|
| 15 |
-
let h = layout.height.max(1.0);
|
| 16 |
-
(x, y, w, h)
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
pub fn show_tab(app: &AppHandle, tab_id: &str, layout: &ViewportLayout) -> Result<(), String> {
|
| 20 |
-
let label = tab_label(app, tab_id)?;
|
| 21 |
-
let (x, y, w, h) = bounds(layout);
|
| 22 |
-
if let Some(webview) = app.get_webview(&label) {
|
| 23 |
-
webview.set_position(LogicalPosition::new(x, y)).map_err(|e| e.to_string())?;
|
| 24 |
-
webview.set_size(LogicalSize::new(w, h)).map_err(|e| e.to_string())?;
|
| 25 |
-
webview.show().map_err(|e| e.to_string())?;
|
| 26 |
-
webview.set_focus().map_err(|e| e.to_string())?;
|
| 27 |
-
}
|
| 28 |
-
Ok(())
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
pub fn hide_tab(app: &AppHandle, tab_id: &str) -> Result<(), String> {
|
| 32 |
-
let label = tab_label(app, tab_id)?;
|
| 33 |
-
if let Some(webview) = app.get_webview(&label) {
|
| 34 |
-
let _ = webview.hide();
|
| 35 |
-
webview.set_position(LogicalPosition::new(HIDDEN_X, HIDDEN_Y)).map_err(|e| e.to_string())?;
|
| 36 |
-
webview.set_size(LogicalSize::new(1.0, 1.0)).map_err(|e| e.to_string())?;
|
| 37 |
-
}
|
| 38 |
-
Ok(())
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
pub fn resize_active(app: &AppHandle, layout: &ViewportLayout) -> Result<(), String> {
|
| 42 |
-
let active = {
|
| 43 |
-
let state = app.state::<AppState>();
|
| 44 |
-
let tabs = state.tabs.lock().map_err(|_| "lock")?;
|
| 45 |
-
tabs.active.clone()
|
| 46 |
-
};
|
| 47 |
-
if let Some(active_id) = active { show_tab(app, &active_id, layout)?; }
|
| 48 |
-
Ok(())
|
| 49 |
-
}
|
|
|
|
| 1 |
+
use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager};
|
| 2 |
+
|
| 3 |
+
use super::tab_manager::{tab_label, ViewportLayout};
|
| 4 |
+
use crate::state::AppState;
|
| 5 |
+
|
| 6 |
+
const HIDDEN_X: f64 = -32000.0;
|
| 7 |
+
const HIDDEN_Y: f64 = -32000.0;
|
| 8 |
+
|
| 9 |
+
/// Calculate child WebView bounds from measured React host rect.
|
| 10 |
+
/// React sends getBoundingClientRect() values in logical/CSS pixels.
|
| 11 |
+
pub fn bounds(layout: &ViewportLayout) -> (f64, f64, f64, f64) {
|
| 12 |
+
let x = layout.x.max(0.0);
|
| 13 |
+
let y = layout.y.max(0.0);
|
| 14 |
+
let w = layout.width.max(1.0);
|
| 15 |
+
let h = layout.height.max(1.0);
|
| 16 |
+
(x, y, w, h)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
pub fn show_tab(app: &AppHandle, tab_id: &str, layout: &ViewportLayout) -> Result<(), String> {
|
| 20 |
+
let label = tab_label(app, tab_id)?;
|
| 21 |
+
let (x, y, w, h) = bounds(layout);
|
| 22 |
+
if let Some(webview) = app.get_webview(&label) {
|
| 23 |
+
webview.set_position(LogicalPosition::new(x, y)).map_err(|e| e.to_string())?;
|
| 24 |
+
webview.set_size(LogicalSize::new(w, h)).map_err(|e| e.to_string())?;
|
| 25 |
+
webview.show().map_err(|e| e.to_string())?;
|
| 26 |
+
webview.set_focus().map_err(|e| e.to_string())?;
|
| 27 |
+
}
|
| 28 |
+
Ok(())
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
pub fn hide_tab(app: &AppHandle, tab_id: &str) -> Result<(), String> {
|
| 32 |
+
let label = tab_label(app, tab_id)?;
|
| 33 |
+
if let Some(webview) = app.get_webview(&label) {
|
| 34 |
+
let _ = webview.hide();
|
| 35 |
+
webview.set_position(LogicalPosition::new(HIDDEN_X, HIDDEN_Y)).map_err(|e| e.to_string())?;
|
| 36 |
+
webview.set_size(LogicalSize::new(1.0, 1.0)).map_err(|e| e.to_string())?;
|
| 37 |
+
}
|
| 38 |
+
Ok(())
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
pub fn resize_active(app: &AppHandle, layout: &ViewportLayout) -> Result<(), String> {
|
| 42 |
+
let active = {
|
| 43 |
+
let state = app.state::<AppState>();
|
| 44 |
+
let tabs = state.tabs.lock().map_err(|_| "lock")?;
|
| 45 |
+
tabs.active.clone()
|
| 46 |
+
};
|
| 47 |
+
if let Some(active_id) = active { show_tab(app, &active_id, layout)?; }
|
| 48 |
+
Ok(())
|
| 49 |
+
}
|
src-tauri/src/browser/mod.rs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
pub mod tab_manager;
|
| 2 |
-
pub mod commands;
|
| 3 |
-
pub mod context_menu;
|
| 4 |
-
pub mod layout;
|
| 5 |
-
pub mod navigation;
|
| 6 |
-
pub mod capture;
|
| 7 |
-
pub mod autofill;
|
| 8 |
-
|
| 9 |
-
pub use tab_manager::*;
|
|
|
|
| 1 |
+
pub mod tab_manager;
|
| 2 |
+
pub mod commands;
|
| 3 |
+
pub mod context_menu;
|
| 4 |
+
pub mod layout;
|
| 5 |
+
pub mod navigation;
|
| 6 |
+
pub mod capture;
|
| 7 |
+
pub mod autofill;
|
| 8 |
+
|
| 9 |
+
pub use tab_manager::*;
|
src-tauri/src/browser/navigation.rs
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
use tauri::{AppHandle, Manager};
|
| 2 |
-
use crate::adblock::engine::AdBlockState;
|
| 3 |
-
|
| 4 |
-
/// Resolve user input to a navigable URL with HTTPS-first
|
| 5 |
-
pub fn resolve_url(input: &str) -> String {
|
| 6 |
-
let trimmed = input.trim();
|
| 7 |
-
if trimmed.is_empty() { return "https://duckduckgo.com".to_string(); }
|
| 8 |
-
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
|
| 9 |
-
// HTTPS-first: upgrade http to https unless localhost
|
| 10 |
-
if trimmed.starts_with("http://") {
|
| 11 |
-
let host = trimmed.split("//").nth(1).and_then(|s| s.split('/').next()).unwrap_or("");
|
| 12 |
-
if host != "localhost" && host != "127.0.0.1" && host != "::1" {
|
| 13 |
-
return trimmed.replacen("http://", "https://", 1);
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
return trimmed.to_string();
|
| 17 |
-
}
|
| 18 |
-
if trimmed.contains('.') && !trimmed.contains(' ') {
|
| 19 |
-
return format!("https://{trimmed}");
|
| 20 |
-
}
|
| 21 |
-
format!("https://duckduckgo.com/?q={}", urlencoded(trimmed))
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
/// Check if a URL should be blocked by adblock at navigation level
|
| 25 |
-
pub fn navigation_blocked(app: &AppHandle, url: &str) -> bool {
|
| 26 |
-
let state = app.state::<AdBlockState>();
|
| 27 |
-
state.should_block(url, url, "document")
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
fn urlencoded(value: &str) -> String {
|
| 31 |
-
value.bytes().flat_map(|b| match b {
|
| 32 |
-
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => vec![b as char],
|
| 33 |
-
b' ' => vec!['+'],
|
| 34 |
-
_ => format!("%{b:02X}").chars().collect(),
|
| 35 |
-
}).collect()
|
| 36 |
-
}
|
|
|
|
| 1 |
+
use tauri::{AppHandle, Manager};
|
| 2 |
+
use crate::adblock::engine::AdBlockState;
|
| 3 |
+
|
| 4 |
+
/// Resolve user input to a navigable URL with HTTPS-first
|
| 5 |
+
pub fn resolve_url(input: &str) -> String {
|
| 6 |
+
let trimmed = input.trim();
|
| 7 |
+
if trimmed.is_empty() { return "https://duckduckgo.com".to_string(); }
|
| 8 |
+
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
|
| 9 |
+
// HTTPS-first: upgrade http to https unless localhost
|
| 10 |
+
if trimmed.starts_with("http://") {
|
| 11 |
+
let host = trimmed.split("//").nth(1).and_then(|s| s.split('/').next()).unwrap_or("");
|
| 12 |
+
if host != "localhost" && host != "127.0.0.1" && host != "::1" {
|
| 13 |
+
return trimmed.replacen("http://", "https://", 1);
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
return trimmed.to_string();
|
| 17 |
+
}
|
| 18 |
+
if trimmed.contains('.') && !trimmed.contains(' ') {
|
| 19 |
+
return format!("https://{trimmed}");
|
| 20 |
+
}
|
| 21 |
+
format!("https://duckduckgo.com/?q={}", urlencoded(trimmed))
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/// Check if a URL should be blocked by adblock at navigation level
|
| 25 |
+
pub fn navigation_blocked(app: &AppHandle, url: &str) -> bool {
|
| 26 |
+
let state = app.state::<AdBlockState>();
|
| 27 |
+
state.should_block(url, url, "document")
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
fn urlencoded(value: &str) -> String {
|
| 31 |
+
value.bytes().flat_map(|b| match b {
|
| 32 |
+
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => vec![b as char],
|
| 33 |
+
b' ' => vec!['+'],
|
| 34 |
+
_ => format!("%{b:02X}").chars().collect(),
|
| 35 |
+
}).collect()
|
| 36 |
+
}
|
src-tauri/src/browser/tab_manager.rs
CHANGED
|
@@ -1,102 +1,102 @@
|
|
| 1 |
-
use serde::{Deserialize, Serialize};
|
| 2 |
-
use std::collections::HashMap;
|
| 3 |
-
use tauri::{AppHandle, Emitter, Manager};
|
| 4 |
-
|
| 5 |
-
use crate::state::AppState;
|
| 6 |
-
|
| 7 |
-
#[allow(dead_code)]
|
| 8 |
-
pub const TAB_SLEEP_THRESHOLD_SECS: i64 = 30 * 60;
|
| 9 |
-
|
| 10 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 11 |
-
pub struct BrowserTab {
|
| 12 |
-
pub id: String,
|
| 13 |
-
pub label: String,
|
| 14 |
-
pub url: String,
|
| 15 |
-
pub title: String,
|
| 16 |
-
pub favicon: Option<String>,
|
| 17 |
-
pub loading: bool,
|
| 18 |
-
pub pinned: bool,
|
| 19 |
-
pub sleeping: bool,
|
| 20 |
-
pub zoom: f64,
|
| 21 |
-
pub can_go_back: bool,
|
| 22 |
-
pub can_go_forward: bool,
|
| 23 |
-
pub last_active: i64,
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
#[derive(Default)]
|
| 27 |
-
pub struct TabManager {
|
| 28 |
-
pub tabs: HashMap<String, BrowserTab>,
|
| 29 |
-
pub order: Vec<String>,
|
| 30 |
-
pub active: Option<String>,
|
| 31 |
-
pub closed_stack: Vec<BrowserTab>,
|
| 32 |
-
pub zoom_memory: HashMap<String, f64>,
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
impl TabManager {
|
| 36 |
-
pub fn push_closed(&mut self, tab: BrowserTab) {
|
| 37 |
-
if self.closed_stack.len() >= 25 { self.closed_stack.remove(0); }
|
| 38 |
-
self.closed_stack.push(tab);
|
| 39 |
-
}
|
| 40 |
-
pub fn pop_closed(&mut self) -> Option<BrowserTab> { self.closed_stack.pop() }
|
| 41 |
-
pub fn remember_zoom(&mut self, domain: &str, zoom: f64) { self.zoom_memory.insert(domain.to_string(), zoom); }
|
| 42 |
-
pub fn get_zoom_for_domain(&self, domain: &str) -> Option<f64> { self.zoom_memory.get(domain).copied() }
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 46 |
-
#[serde(rename_all = "camelCase")]
|
| 47 |
-
pub struct ViewportLayout {
|
| 48 |
-
#[serde(default)]
|
| 49 |
-
pub x: f64,
|
| 50 |
-
#[serde(default)]
|
| 51 |
-
pub y: f64,
|
| 52 |
-
pub width: f64,
|
| 53 |
-
pub height: f64,
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
#[derive(Debug, Clone, Serialize)]
|
| 57 |
-
pub struct BrowserSnapshot {
|
| 58 |
-
pub tabs: Vec<BrowserTab>,
|
| 59 |
-
pub active: Option<String>,
|
| 60 |
-
pub can_restore: bool,
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
pub fn snapshot(app: &AppHandle) -> Result<BrowserSnapshot, String> {
|
| 64 |
-
let state = app.state::<AppState>();
|
| 65 |
-
let tabs = state.tabs.lock().map_err(|_| "lock")?;
|
| 66 |
-
let ordered = tabs.order.iter().filter_map(|id| tabs.tabs.get(id).cloned()).collect();
|
| 67 |
-
let can_restore = !tabs.closed_stack.is_empty();
|
| 68 |
-
Ok(BrowserSnapshot { tabs: ordered, active: tabs.active.clone(), can_restore })
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
pub fn emit_snapshot(app: &AppHandle) -> Result<(), String> {
|
| 72 |
-
let snap = snapshot(app)?;
|
| 73 |
-
app.emit("browser://tabs", snap).map_err(|e| e.to_string())
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
pub fn update_tab_field(app: &AppHandle, id: &str, f: impl FnOnce(&mut BrowserTab)) {
|
| 77 |
-
let state = app.state::<AppState>();
|
| 78 |
-
if let Ok(mut tabs) = state.tabs.lock() {
|
| 79 |
-
if let Some(tab) = tabs.tabs.get_mut(id) { f(tab); }
|
| 80 |
-
}
|
| 81 |
-
let _ = emit_snapshot(app);
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
pub fn tab_label(app: &AppHandle, tab_id: &str) -> Result<String, String> {
|
| 85 |
-
let state = app.state::<AppState>();
|
| 86 |
-
let tabs = state.tabs.lock().map_err(|_| "lock")?;
|
| 87 |
-
tabs.tabs.get(tab_id).map(|t| t.label.clone()).ok_or_else(|| format!("tab not found: {tab_id}"))
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
pub fn eval_on_tab(app: &AppHandle, tab_id: &str, js: &str) -> Result<(), String> {
|
| 91 |
-
let label = tab_label(app, tab_id)?;
|
| 92 |
-
app.get_webview(&label).ok_or("webview not found")?.eval(js).map_err(|e| e.to_string())
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
pub fn extract_domain(url: &str) -> String {
|
| 96 |
-
url.split("//").nth(1)
|
| 97 |
-
.and_then(|s| s.split('/').next())
|
| 98 |
-
.map(|s| s.split(':').next().unwrap_or(s))
|
| 99 |
-
.unwrap_or("")
|
| 100 |
-
.trim_start_matches("www.")
|
| 101 |
-
.to_lowercase()
|
| 102 |
-
}
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use std::collections::HashMap;
|
| 3 |
+
use tauri::{AppHandle, Emitter, Manager};
|
| 4 |
+
|
| 5 |
+
use crate::state::AppState;
|
| 6 |
+
|
| 7 |
+
#[allow(dead_code)]
|
| 8 |
+
pub const TAB_SLEEP_THRESHOLD_SECS: i64 = 30 * 60;
|
| 9 |
+
|
| 10 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 11 |
+
pub struct BrowserTab {
|
| 12 |
+
pub id: String,
|
| 13 |
+
pub label: String,
|
| 14 |
+
pub url: String,
|
| 15 |
+
pub title: String,
|
| 16 |
+
pub favicon: Option<String>,
|
| 17 |
+
pub loading: bool,
|
| 18 |
+
pub pinned: bool,
|
| 19 |
+
pub sleeping: bool,
|
| 20 |
+
pub zoom: f64,
|
| 21 |
+
pub can_go_back: bool,
|
| 22 |
+
pub can_go_forward: bool,
|
| 23 |
+
pub last_active: i64,
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
#[derive(Default)]
|
| 27 |
+
pub struct TabManager {
|
| 28 |
+
pub tabs: HashMap<String, BrowserTab>,
|
| 29 |
+
pub order: Vec<String>,
|
| 30 |
+
pub active: Option<String>,
|
| 31 |
+
pub closed_stack: Vec<BrowserTab>,
|
| 32 |
+
pub zoom_memory: HashMap<String, f64>,
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
impl TabManager {
|
| 36 |
+
pub fn push_closed(&mut self, tab: BrowserTab) {
|
| 37 |
+
if self.closed_stack.len() >= 25 { self.closed_stack.remove(0); }
|
| 38 |
+
self.closed_stack.push(tab);
|
| 39 |
+
}
|
| 40 |
+
pub fn pop_closed(&mut self) -> Option<BrowserTab> { self.closed_stack.pop() }
|
| 41 |
+
pub fn remember_zoom(&mut self, domain: &str, zoom: f64) { self.zoom_memory.insert(domain.to_string(), zoom); }
|
| 42 |
+
pub fn get_zoom_for_domain(&self, domain: &str) -> Option<f64> { self.zoom_memory.get(domain).copied() }
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 46 |
+
#[serde(rename_all = "camelCase")]
|
| 47 |
+
pub struct ViewportLayout {
|
| 48 |
+
#[serde(default)]
|
| 49 |
+
pub x: f64,
|
| 50 |
+
#[serde(default)]
|
| 51 |
+
pub y: f64,
|
| 52 |
+
pub width: f64,
|
| 53 |
+
pub height: f64,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
#[derive(Debug, Clone, Serialize)]
|
| 57 |
+
pub struct BrowserSnapshot {
|
| 58 |
+
pub tabs: Vec<BrowserTab>,
|
| 59 |
+
pub active: Option<String>,
|
| 60 |
+
pub can_restore: bool,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
pub fn snapshot(app: &AppHandle) -> Result<BrowserSnapshot, String> {
|
| 64 |
+
let state = app.state::<AppState>();
|
| 65 |
+
let tabs = state.tabs.lock().map_err(|_| "lock")?;
|
| 66 |
+
let ordered = tabs.order.iter().filter_map(|id| tabs.tabs.get(id).cloned()).collect();
|
| 67 |
+
let can_restore = !tabs.closed_stack.is_empty();
|
| 68 |
+
Ok(BrowserSnapshot { tabs: ordered, active: tabs.active.clone(), can_restore })
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
pub fn emit_snapshot(app: &AppHandle) -> Result<(), String> {
|
| 72 |
+
let snap = snapshot(app)?;
|
| 73 |
+
app.emit("browser://tabs", snap).map_err(|e| e.to_string())
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
pub fn update_tab_field(app: &AppHandle, id: &str, f: impl FnOnce(&mut BrowserTab)) {
|
| 77 |
+
let state = app.state::<AppState>();
|
| 78 |
+
if let Ok(mut tabs) = state.tabs.lock() {
|
| 79 |
+
if let Some(tab) = tabs.tabs.get_mut(id) { f(tab); }
|
| 80 |
+
}
|
| 81 |
+
let _ = emit_snapshot(app);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
pub fn tab_label(app: &AppHandle, tab_id: &str) -> Result<String, String> {
|
| 85 |
+
let state = app.state::<AppState>();
|
| 86 |
+
let tabs = state.tabs.lock().map_err(|_| "lock")?;
|
| 87 |
+
tabs.tabs.get(tab_id).map(|t| t.label.clone()).ok_or_else(|| format!("tab not found: {tab_id}"))
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
pub fn eval_on_tab(app: &AppHandle, tab_id: &str, js: &str) -> Result<(), String> {
|
| 91 |
+
let label = tab_label(app, tab_id)?;
|
| 92 |
+
app.get_webview(&label).ok_or("webview not found")?.eval(js).map_err(|e| e.to_string())
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
pub fn extract_domain(url: &str) -> String {
|
| 96 |
+
url.split("//").nth(1)
|
| 97 |
+
.and_then(|s| s.split('/').next())
|
| 98 |
+
.map(|s| s.split(':').next().unwrap_or(s))
|
| 99 |
+
.unwrap_or("")
|
| 100 |
+
.trim_start_matches("www.")
|
| 101 |
+
.to_lowercase()
|
| 102 |
+
}
|
src-tauri/src/color_tools.rs
CHANGED
|
@@ -1,108 +1,108 @@
|
|
| 1 |
-
use serde::{Deserialize, Serialize};
|
| 2 |
-
use tauri::{AppHandle, Manager};
|
| 3 |
-
|
| 4 |
-
/// Export palette colors to various artist application formats
|
| 5 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 6 |
-
pub struct ColorExportResult {
|
| 7 |
-
pub format: String,
|
| 8 |
-
pub content: String,
|
| 9 |
-
pub filename: String,
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
#[tauri::command]
|
| 13 |
-
pub fn color_export(colors: Vec<String>, format: String) -> Result<ColorExportResult, String> {
|
| 14 |
-
let content = match format.as_str() {
|
| 15 |
-
"hex" => colors.join("\n"),
|
| 16 |
-
"css" => colors.iter().enumerate()
|
| 17 |
-
.map(|(i, c)| format!(" --color-{}: {};", i + 1, c))
|
| 18 |
-
.collect::<Vec<_>>()
|
| 19 |
-
.join("\n")
|
| 20 |
-
.pipe(|body| format!(":root {{\n{body}\n}}")),
|
| 21 |
-
"gpl" => {
|
| 22 |
-
let mut lines = vec!["GIMP Palette".to_string(), "Name: Muse Export".to_string(), "#".to_string()];
|
| 23 |
-
for color in &colors {
|
| 24 |
-
if let Some((r, g, b)) = parse_hex(color) {
|
| 25 |
-
lines.push(format!("{r:3} {g:3} {b:3}\t{color}"));
|
| 26 |
-
}
|
| 27 |
-
}
|
| 28 |
-
lines.join("\n")
|
| 29 |
-
}
|
| 30 |
-
"ase" => {
|
| 31 |
-
let mut lines = vec!["// Adobe Swatch Exchange (text preview)".to_string()];
|
| 32 |
-
for color in &colors {
|
| 33 |
-
if let Some((r, g, b)) = parse_hex(color) {
|
| 34 |
-
lines.push(format!("RGB {:.4} {:.4} {:.4} // {color}", r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0));
|
| 35 |
-
}
|
| 36 |
-
}
|
| 37 |
-
lines.join("\n")
|
| 38 |
-
}
|
| 39 |
-
"procreate" => {
|
| 40 |
-
let swatches: Vec<serde_json::Value> = colors.iter().filter_map(|c| {
|
| 41 |
-
parse_hex(c).map(|(r, g, b)| serde_json::json!({
|
| 42 |
-
"hue": 0, "saturation": 0, "brightness": 0,
|
| 43 |
-
"alpha": 1, "colorSpace": 0,
|
| 44 |
-
"components": [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0, 1.0]
|
| 45 |
-
}))
|
| 46 |
-
}).collect();
|
| 47 |
-
serde_json::to_string_pretty(&serde_json::json!({
|
| 48 |
-
"name": "Muse Export",
|
| 49 |
-
"swatches": swatches
|
| 50 |
-
})).unwrap_or_default()
|
| 51 |
-
}
|
| 52 |
-
_ => colors.join("\n"),
|
| 53 |
-
};
|
| 54 |
-
|
| 55 |
-
let ext = match format.as_str() {
|
| 56 |
-
"css" => "css", "gpl" => "gpl", "ase" => "ase", "procreate" => "swatches", _ => "txt"
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
Ok(ColorExportResult {
|
| 60 |
-
format: format.clone(),
|
| 61 |
-
content,
|
| 62 |
-
filename: format!("muse-palette.{ext}"),
|
| 63 |
-
})
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
#[tauri::command]
|
| 67 |
-
pub fn color_search_library(app: AppHandle, hue_min: f32, hue_max: f32) -> Result<Vec<crate::library::LibraryItem>, String> {
|
| 68 |
-
let state = app.state::<crate::library::LibraryState>();
|
| 69 |
-
let items = state.items.lock().map_err(|_| "library lock poisoned")?;
|
| 70 |
-
Ok(items.iter().filter(|item| {
|
| 71 |
-
item.colors.iter().any(|c| {
|
| 72 |
-
if let Some((r, g, b)) = parse_hex(c) {
|
| 73 |
-
let hue = rgb_to_hue(r, g, b);
|
| 74 |
-
if hue_min <= hue_max {
|
| 75 |
-
hue >= hue_min && hue <= hue_max
|
| 76 |
-
} else {
|
| 77 |
-
hue >= hue_min || hue <= hue_max
|
| 78 |
-
}
|
| 79 |
-
} else { false }
|
| 80 |
-
})
|
| 81 |
-
}).cloned().collect())
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
|
| 85 |
-
let h = hex.trim_start_matches('#');
|
| 86 |
-
if h.len() != 6 { return None; }
|
| 87 |
-
let r = u8::from_str_radix(&h[0..2], 16).ok()?;
|
| 88 |
-
let g = u8::from_str_radix(&h[2..4], 16).ok()?;
|
| 89 |
-
let b = u8::from_str_radix(&h[4..6], 16).ok()?;
|
| 90 |
-
Some((r, g, b))
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
fn rgb_to_hue(r: u8, g: u8, b: u8) -> f32 {
|
| 94 |
-
let rf = r as f32 / 255.0;
|
| 95 |
-
let gf = g as f32 / 255.0;
|
| 96 |
-
let bf = b as f32 / 255.0;
|
| 97 |
-
let max = rf.max(gf).max(bf);
|
| 98 |
-
let min = rf.min(gf).min(bf);
|
| 99 |
-
if max == min { return 0.0; }
|
| 100 |
-
let d = max - min;
|
| 101 |
-
let h = if max == rf { ((gf - bf) / d) % 6.0 }
|
| 102 |
-
else if max == gf { (bf - rf) / d + 2.0 }
|
| 103 |
-
else { (rf - gf) / d + 4.0 };
|
| 104 |
-
((h * 60.0) + 360.0) % 360.0
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
trait PipeExt { fn pipe(self, f: impl FnOnce(String) -> String) -> String; }
|
| 108 |
-
impl PipeExt for String { fn pipe(self, f: impl FnOnce(String) -> String) -> String { f(self) } }
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use tauri::{AppHandle, Manager};
|
| 3 |
+
|
| 4 |
+
/// Export palette colors to various artist application formats
|
| 5 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 6 |
+
pub struct ColorExportResult {
|
| 7 |
+
pub format: String,
|
| 8 |
+
pub content: String,
|
| 9 |
+
pub filename: String,
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
#[tauri::command]
|
| 13 |
+
pub fn color_export(colors: Vec<String>, format: String) -> Result<ColorExportResult, String> {
|
| 14 |
+
let content = match format.as_str() {
|
| 15 |
+
"hex" => colors.join("\n"),
|
| 16 |
+
"css" => colors.iter().enumerate()
|
| 17 |
+
.map(|(i, c)| format!(" --color-{}: {};", i + 1, c))
|
| 18 |
+
.collect::<Vec<_>>()
|
| 19 |
+
.join("\n")
|
| 20 |
+
.pipe(|body| format!(":root {{\n{body}\n}}")),
|
| 21 |
+
"gpl" => {
|
| 22 |
+
let mut lines = vec!["GIMP Palette".to_string(), "Name: Muse Export".to_string(), "#".to_string()];
|
| 23 |
+
for color in &colors {
|
| 24 |
+
if let Some((r, g, b)) = parse_hex(color) {
|
| 25 |
+
lines.push(format!("{r:3} {g:3} {b:3}\t{color}"));
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
lines.join("\n")
|
| 29 |
+
}
|
| 30 |
+
"ase" => {
|
| 31 |
+
let mut lines = vec!["// Adobe Swatch Exchange (text preview)".to_string()];
|
| 32 |
+
for color in &colors {
|
| 33 |
+
if let Some((r, g, b)) = parse_hex(color) {
|
| 34 |
+
lines.push(format!("RGB {:.4} {:.4} {:.4} // {color}", r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0));
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
lines.join("\n")
|
| 38 |
+
}
|
| 39 |
+
"procreate" => {
|
| 40 |
+
let swatches: Vec<serde_json::Value> = colors.iter().filter_map(|c| {
|
| 41 |
+
parse_hex(c).map(|(r, g, b)| serde_json::json!({
|
| 42 |
+
"hue": 0, "saturation": 0, "brightness": 0,
|
| 43 |
+
"alpha": 1, "colorSpace": 0,
|
| 44 |
+
"components": [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0, 1.0]
|
| 45 |
+
}))
|
| 46 |
+
}).collect();
|
| 47 |
+
serde_json::to_string_pretty(&serde_json::json!({
|
| 48 |
+
"name": "Muse Export",
|
| 49 |
+
"swatches": swatches
|
| 50 |
+
})).unwrap_or_default()
|
| 51 |
+
}
|
| 52 |
+
_ => colors.join("\n"),
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
let ext = match format.as_str() {
|
| 56 |
+
"css" => "css", "gpl" => "gpl", "ase" => "ase", "procreate" => "swatches", _ => "txt"
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
Ok(ColorExportResult {
|
| 60 |
+
format: format.clone(),
|
| 61 |
+
content,
|
| 62 |
+
filename: format!("muse-palette.{ext}"),
|
| 63 |
+
})
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
#[tauri::command]
|
| 67 |
+
pub fn color_search_library(app: AppHandle, hue_min: f32, hue_max: f32) -> Result<Vec<crate::library::LibraryItem>, String> {
|
| 68 |
+
let state = app.state::<crate::library::LibraryState>();
|
| 69 |
+
let items = state.items.lock().map_err(|_| "library lock poisoned")?;
|
| 70 |
+
Ok(items.iter().filter(|item| {
|
| 71 |
+
item.colors.iter().any(|c| {
|
| 72 |
+
if let Some((r, g, b)) = parse_hex(c) {
|
| 73 |
+
let hue = rgb_to_hue(r, g, b);
|
| 74 |
+
if hue_min <= hue_max {
|
| 75 |
+
hue >= hue_min && hue <= hue_max
|
| 76 |
+
} else {
|
| 77 |
+
hue >= hue_min || hue <= hue_max
|
| 78 |
+
}
|
| 79 |
+
} else { false }
|
| 80 |
+
})
|
| 81 |
+
}).cloned().collect())
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
|
| 85 |
+
let h = hex.trim_start_matches('#');
|
| 86 |
+
if h.len() != 6 { return None; }
|
| 87 |
+
let r = u8::from_str_radix(&h[0..2], 16).ok()?;
|
| 88 |
+
let g = u8::from_str_radix(&h[2..4], 16).ok()?;
|
| 89 |
+
let b = u8::from_str_radix(&h[4..6], 16).ok()?;
|
| 90 |
+
Some((r, g, b))
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
fn rgb_to_hue(r: u8, g: u8, b: u8) -> f32 {
|
| 94 |
+
let rf = r as f32 / 255.0;
|
| 95 |
+
let gf = g as f32 / 255.0;
|
| 96 |
+
let bf = b as f32 / 255.0;
|
| 97 |
+
let max = rf.max(gf).max(bf);
|
| 98 |
+
let min = rf.min(gf).min(bf);
|
| 99 |
+
if max == min { return 0.0; }
|
| 100 |
+
let d = max - min;
|
| 101 |
+
let h = if max == rf { ((gf - bf) / d) % 6.0 }
|
| 102 |
+
else if max == gf { (bf - rf) / d + 2.0 }
|
| 103 |
+
else { (rf - gf) / d + 4.0 };
|
| 104 |
+
((h * 60.0) + 360.0) % 360.0
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
trait PipeExt { fn pipe(self, f: impl FnOnce(String) -> String) -> String; }
|
| 108 |
+
impl PipeExt for String { fn pipe(self, f: impl FnOnce(String) -> String) -> String { f(self) } }
|
src-tauri/src/credentials.rs
CHANGED
|
@@ -1,31 +1,31 @@
|
|
| 1 |
-
use serde::{Deserialize, Serialize};
|
| 2 |
-
|
| 3 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 4 |
-
pub struct CredentialEntry {
|
| 5 |
-
pub id: String,
|
| 6 |
-
pub domain: String,
|
| 7 |
-
pub username: String,
|
| 8 |
-
pub vault_key: String,
|
| 9 |
-
pub label: String,
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
/// Credentials are managed via frontend Stronghold JS API + SQLite metadata index.
|
| 13 |
-
/// Rust side provides helper commands for the metadata layer.
|
| 14 |
-
|
| 15 |
-
#[tauri::command]
|
| 16 |
-
pub fn credentials_list() -> Result<Vec<CredentialEntry>, String> {
|
| 17 |
-
// Credentials stored via frontend Stronghold + SQL plugin
|
| 18 |
-
Ok(vec![])
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
#[tauri::command]
|
| 22 |
-
pub fn credentials_generate_password(length: u32) -> String {
|
| 23 |
-
let charset: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*-_=+".chars().collect();
|
| 24 |
-
let len = length.max(8).min(64) as usize;
|
| 25 |
-
let mut password = String::with_capacity(len);
|
| 26 |
-
for i in 0..len {
|
| 27 |
-
let seed = (chrono::Utc::now().timestamp_subsec_nanos() as usize).wrapping_mul(7 + i * 13);
|
| 28 |
-
password.push(charset[seed % charset.len()]);
|
| 29 |
-
}
|
| 30 |
-
password
|
| 31 |
-
}
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
|
| 3 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 4 |
+
pub struct CredentialEntry {
|
| 5 |
+
pub id: String,
|
| 6 |
+
pub domain: String,
|
| 7 |
+
pub username: String,
|
| 8 |
+
pub vault_key: String,
|
| 9 |
+
pub label: String,
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/// Credentials are managed via frontend Stronghold JS API + SQLite metadata index.
|
| 13 |
+
/// Rust side provides helper commands for the metadata layer.
|
| 14 |
+
|
| 15 |
+
#[tauri::command]
|
| 16 |
+
pub fn credentials_list() -> Result<Vec<CredentialEntry>, String> {
|
| 17 |
+
// Credentials stored via frontend Stronghold + SQL plugin
|
| 18 |
+
Ok(vec![])
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
#[tauri::command]
|
| 22 |
+
pub fn credentials_generate_password(length: u32) -> String {
|
| 23 |
+
let charset: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*-_=+".chars().collect();
|
| 24 |
+
let len = length.max(8).min(64) as usize;
|
| 25 |
+
let mut password = String::with_capacity(len);
|
| 26 |
+
for i in 0..len {
|
| 27 |
+
let seed = (chrono::Utc::now().timestamp_subsec_nanos() as usize).wrapping_mul(7 + i * 13);
|
| 28 |
+
password.push(charset[seed % charset.len()]);
|
| 29 |
+
}
|
| 30 |
+
password
|
| 31 |
+
}
|
src-tauri/src/downloads.rs
CHANGED
|
@@ -1,75 +1,75 @@
|
|
| 1 |
-
use serde::{Deserialize, Serialize};
|
| 2 |
-
use std::sync::Mutex;
|
| 3 |
-
use tauri::{AppHandle, Emitter, Manager};
|
| 4 |
-
use uuid::Uuid;
|
| 5 |
-
|
| 6 |
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 7 |
-
pub struct DownloadRecord {
|
| 8 |
-
pub id: String,
|
| 9 |
-
pub url: String,
|
| 10 |
-
pub filename: String,
|
| 11 |
-
pub status: String,
|
| 12 |
-
pub saved_to_library: bool,
|
| 13 |
-
pub created_at: i64,
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
#[derive(Default)]
|
| 17 |
-
pub struct DownloadState {
|
| 18 |
-
pub records: Mutex<Vec<DownloadRecord>>,
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
#[tauri::command]
|
| 22 |
-
pub fn downloads_list(app: AppHandle) -> Result<Vec<DownloadRecord>, String> {
|
| 23 |
-
let state = app.state::<DownloadState>();
|
| 24 |
-
let records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 25 |
-
Ok(records.clone())
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
#[tauri::command]
|
| 29 |
-
pub fn downloads_clear_completed(app: AppHandle) -> Result<(), String> {
|
| 30 |
-
let state = app.state::<DownloadState>();
|
| 31 |
-
let mut records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 32 |
-
records.retain(|r| r.status == "active");
|
| 33 |
-
Ok(())
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
#[tauri::command]
|
| 37 |
-
pub async fn download_to_library(app: AppHandle, url: String) -> Result<crate::library::LibraryItem, String> {
|
| 38 |
-
let filename = url.split('/').last().unwrap_or("download").to_string();
|
| 39 |
-
let record = DownloadRecord {
|
| 40 |
-
id: Uuid::new_v4().to_string(),
|
| 41 |
-
url: url.clone(),
|
| 42 |
-
filename: filename.clone(),
|
| 43 |
-
status: "active".to_string(),
|
| 44 |
-
saved_to_library: true,
|
| 45 |
-
created_at: chrono::Utc::now().timestamp(),
|
| 46 |
-
};
|
| 47 |
-
{
|
| 48 |
-
let state = app.state::<DownloadState>();
|
| 49 |
-
let mut records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 50 |
-
records.push(record.clone());
|
| 51 |
-
}
|
| 52 |
-
let _ = app.emit("downloads://update", &record);
|
| 53 |
-
let item = crate::library::library_add_item(app.clone(), url, Some(record.url.clone()), Some(filename)).await?;
|
| 54 |
-
{
|
| 55 |
-
let state = app.state::<DownloadState>();
|
| 56 |
-
let mut records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 57 |
-
if let Some(r) = records.iter_mut().find(|r| r.id == record.id) {
|
| 58 |
-
r.status = "complete".to_string();
|
| 59 |
-
}
|
| 60 |
-
}
|
| 61 |
-
let _ = app.emit("downloads://complete", &record.id);
|
| 62 |
-
Ok(item)
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
#[tauri::command]
|
| 66 |
-
pub fn web_clip_page(url: String, title: String) -> DownloadRecord {
|
| 67 |
-
DownloadRecord {
|
| 68 |
-
id: Uuid::new_v4().to_string(),
|
| 69 |
-
url,
|
| 70 |
-
filename: title,
|
| 71 |
-
status: "clipped".to_string(),
|
| 72 |
-
saved_to_library: false,
|
| 73 |
-
created_at: chrono::Utc::now().timestamp(),
|
| 74 |
-
}
|
| 75 |
-
}
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use std::sync::Mutex;
|
| 3 |
+
use tauri::{AppHandle, Emitter, Manager};
|
| 4 |
+
use uuid::Uuid;
|
| 5 |
+
|
| 6 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 7 |
+
pub struct DownloadRecord {
|
| 8 |
+
pub id: String,
|
| 9 |
+
pub url: String,
|
| 10 |
+
pub filename: String,
|
| 11 |
+
pub status: String,
|
| 12 |
+
pub saved_to_library: bool,
|
| 13 |
+
pub created_at: i64,
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
#[derive(Default)]
|
| 17 |
+
pub struct DownloadState {
|
| 18 |
+
pub records: Mutex<Vec<DownloadRecord>>,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
#[tauri::command]
|
| 22 |
+
pub fn downloads_list(app: AppHandle) -> Result<Vec<DownloadRecord>, String> {
|
| 23 |
+
let state = app.state::<DownloadState>();
|
| 24 |
+
let records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 25 |
+
Ok(records.clone())
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
#[tauri::command]
|
| 29 |
+
pub fn downloads_clear_completed(app: AppHandle) -> Result<(), String> {
|
| 30 |
+
let state = app.state::<DownloadState>();
|
| 31 |
+
let mut records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 32 |
+
records.retain(|r| r.status == "active");
|
| 33 |
+
Ok(())
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
#[tauri::command]
|
| 37 |
+
pub async fn download_to_library(app: AppHandle, url: String) -> Result<crate::library::LibraryItem, String> {
|
| 38 |
+
let filename = url.split('/').last().unwrap_or("download").to_string();
|
| 39 |
+
let record = DownloadRecord {
|
| 40 |
+
id: Uuid::new_v4().to_string(),
|
| 41 |
+
url: url.clone(),
|
| 42 |
+
filename: filename.clone(),
|
| 43 |
+
status: "active".to_string(),
|
| 44 |
+
saved_to_library: true,
|
| 45 |
+
created_at: chrono::Utc::now().timestamp(),
|
| 46 |
+
};
|
| 47 |
+
{
|
| 48 |
+
let state = app.state::<DownloadState>();
|
| 49 |
+
let mut records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 50 |
+
records.push(record.clone());
|
| 51 |
+
}
|
| 52 |
+
let _ = app.emit("downloads://update", &record);
|
| 53 |
+
let item = crate::library::library_add_item(app.clone(), url, Some(record.url.clone()), Some(filename)).await?;
|
| 54 |
+
{
|
| 55 |
+
let state = app.state::<DownloadState>();
|
| 56 |
+
let mut records = state.records.lock().map_err(|_| "download lock poisoned")?;
|
| 57 |
+
if let Some(r) = records.iter_mut().find(|r| r.id == record.id) {
|
| 58 |
+
r.status = "complete".to_string();
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
let _ = app.emit("downloads://complete", &record.id);
|
| 62 |
+
Ok(item)
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
#[tauri::command]
|
| 66 |
+
pub fn web_clip_page(url: String, title: String) -> DownloadRecord {
|
| 67 |
+
DownloadRecord {
|
| 68 |
+
id: Uuid::new_v4().to_string(),
|
| 69 |
+
url,
|
| 70 |
+
filename: title,
|
| 71 |
+
status: "clipped".to_string(),
|
| 72 |
+
saved_to_library: false,
|
| 73 |
+
created_at: chrono::Utc::now().timestamp(),
|
| 74 |
+
}
|
| 75 |
+
}
|