asdf98 commited on
Commit
3d7d9b5
·
verified ·
1 Parent(s): a28544d

Upload 112 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +36 -36
  2. .gitignore +7 -7
  3. README.md +98 -98
  4. deno.json +19 -19
  5. deno.lock +1128 -0
  6. docs/ARCHITECTURE_PIVOT.md +43 -43
  7. docs/MUSE_SRS_v3.md +205 -205
  8. docs/MUSE_SRS_v4.md +170 -170
  9. index.html +12 -12
  10. package.json +36 -36
  11. src-tauri/Cargo.lock +0 -0
  12. src-tauri/Cargo.toml +2 -0
  13. src-tauri/build.rs +3 -3
  14. src-tauri/capabilities/default.json +29 -29
  15. src-tauri/gen/schemas/acl-manifests.json +0 -0
  16. src-tauri/gen/schemas/capabilities.json +1 -0
  17. src-tauri/gen/schemas/desktop-schema.json +0 -0
  18. src-tauri/gen/schemas/windows-schema.json +0 -0
  19. src-tauri/icons/README.md +1 -1
  20. src-tauri/migrations/001_phase0_init.sql +21 -21
  21. src-tauri/migrations/002_phase3_tables.sql +94 -94
  22. src-tauri/resources/filters/annoyances_mini.txt +20 -20
  23. src-tauri/resources/filters/easylist_mini.txt +51 -51
  24. src-tauri/resources/filters/easyprivacy_mini.txt +28 -28
  25. src-tauri/resources/scriptlets/muse_ubo_compatible_scriptlets.js +105 -105
  26. src-tauri/resources/scripts/adblock_layer1.js +141 -141
  27. src-tauri/resources/scripts/autofill_suppress.js +71 -71
  28. src-tauri/resources/scripts/canvas_noise.js +42 -42
  29. src-tauri/resources/scripts/cookie_consent.js +72 -72
  30. src-tauri/resources/scripts/hover_overlay.js +144 -144
  31. src-tauri/resources/scripts/vault_detector.js +159 -159
  32. src-tauri/resources/scripts/video_ad_scriptlets.js +314 -314
  33. src-tauri/resources/scripts/webrtc_protect.js +24 -24
  34. src-tauri/src/adblock/commands.rs +64 -64
  35. src-tauri/src/adblock/engine.rs +154 -154
  36. src-tauri/src/adblock/mod.rs +4 -4
  37. src-tauri/src/adblock/scripts.rs +71 -71
  38. src-tauri/src/adblock/updater.rs +103 -103
  39. src-tauri/src/board.rs +222 -222
  40. src-tauri/src/browser/autofill.rs +13 -13
  41. src-tauri/src/browser/capture.rs +93 -93
  42. src-tauri/src/browser/commands.rs +272 -225
  43. src-tauri/src/browser/context_menu.rs +23 -23
  44. src-tauri/src/browser/layout.rs +49 -49
  45. src-tauri/src/browser/mod.rs +9 -9
  46. src-tauri/src/browser/navigation.rs +36 -36
  47. src-tauri/src/browser/tab_manager.rs +102 -102
  48. src-tauri/src/color_tools.rs +108 -108
  49. src-tauri/src/credentials.rs +31 -31
  50. 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.9.0",
14
- "@tauri-apps/plugin-clipboard-manager": "^2.3.0",
15
- "@tauri-apps/plugin-fs": "^2.4.0",
16
- "@tauri-apps/plugin-opener": "^2.5.0",
17
- "@tauri-apps/plugin-store": "^2.4.0",
18
- "@tauri-apps/plugin-stronghold": "^2.3.0",
19
- "lucide-react": "^0.546.0",
20
- "motion": "^12.23.0",
21
- "react": "^19.0.0",
22
- "react-dom": "^19.0.0"
23
- },
24
- "devDependencies": {
25
- "@tauri-apps/cli": "^2.9.0",
26
- "@tailwindcss/vite": "^4.1.0",
27
- "@types/react": "^19.0.0",
28
- "@types/react-dom": "^19.0.0",
29
- "@vitejs/plugin-react": "^5.0.0",
30
- "autoprefixer": "^10.4.21",
31
- "postcss": "^8.5.0",
32
- "tailwindcss": "^4.1.0",
33
- "typescript": "~5.8.2",
34
- "vite": "^6.2.0"
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
- pub(crate) async fn create_tab_inner(app: &AppHandle, url: &str, layout: &ViewportLayout, show_immediately: bool) -> Result<String, String> {
130
- let id_num = app.state::<AppState>().next_tab_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
131
- let id = format!("tab-{id_num}");
132
- let label = format!("muse-tab-{id_num}");
133
- let parsed = Url::parse(url).map_err(|e| e.to_string())?;
134
- let init_script = scripts::build_init_script(&scripts::blocked_domains_json());
135
- let full_init = format!("{init_script}\n{CONTEXT_MENU_BLOCK_JS}\n{FAVICON_SCRIPT}");
136
-
137
- let app_for_load = app.clone();
138
- let id_for_load = id.clone();
139
- let app_for_title = app.clone();
140
- let id_for_title = id.clone();
141
- let app_for_cosmetic = app.clone();
142
-
143
- let builder = WebviewBuilder::new(label.clone(), WebviewUrl::External(parsed))
144
- .initialization_script(&full_init)
145
- .on_page_load(move |webview, payload| {
146
- let url = payload.url().to_string();
147
- let loading = matches!(payload.event(), PageLoadEvent::Started);
148
- update_tab_field(&app_for_load, &id_for_load, |t| { t.url = url.clone(); t.loading = loading; });
149
- if matches!(payload.event(), PageLoadEvent::Finished) {
150
- let _ = webview.eval(CONTEXT_MENU_BLOCK_JS);
151
- let _ = webview.eval(scripts::hover_overlay_script());
152
- let adblock_state = app_for_cosmetic.state::<AdBlockState>();
153
- let css = adblock_state.get_cosmetic_css(&url);
154
- if !css.is_empty() {
155
- let escaped = css.replace('\\', "\\\\").replace('`', "\\`");
156
- let _ = webview.eval(&format!("(function(){{const s=document.createElement('style');s.id='__muse_shield';s.textContent=`{escaped}`;document.head.appendChild(s)}})();"));
157
- }
158
- let scriptlet_js = adblock_state.get_injected_script(&url);
159
- if !scriptlet_js.is_empty() { let _ = webview.eval(&format!("try{{{scriptlet_js}}}catch(e){{}}")); }
160
- let _ = webview.eval("window.__muse_report_favicon && window.__muse_report_favicon()");
161
- }
162
- })
163
- .on_document_title_changed(move |_webview, title| { update_tab_field(&app_for_title, &id_for_title, |t| { if !title.trim().is_empty() { t.title = title.clone(); } }); })
164
- .on_navigation({ let app_nav = app.clone(); move |url| {
165
- let s = url.as_str();
166
- if s.starts_with("muse-action://") { handle_muse_action(&app_nav, s); return false; }
167
- !navigation_blocked(&app_nav, s)
168
- }});
169
-
170
- let window = app.get_window("main").ok_or("main window not found")?;
171
- let (cx, cy, cw, ch) = if show_immediately && layout.width > 10.0 { bounds(layout) } else { (-32000.0, -32000.0, 1.0, 1.0) };
172
- window.add_child(builder, LogicalPosition::new(cx, cy), LogicalSize::new(cw, ch)).map_err(|e| e.to_string())?;
173
- if !show_immediately { if let Some(webview) = app.get_webview(&label) { let _ = webview.hide(); } }
174
-
175
- let domain = extract_domain(url);
176
- let saved_zoom = { let state = app.state::<AppState>(); let tabs = state.tabs.lock().map_err(|_| "lock")?; tabs.get_zoom_for_domain(&domain) };
177
- let now = chrono::Utc::now().timestamp();
178
- let zoom = saved_zoom.unwrap_or(1.0);
179
- 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 };
180
- if zoom != 1.0 { if let Some(webview) = app.get_webview(&label) { let _ = webview.set_zoom(zoom); } }
181
- if let Some(prev) = previous { if prev != id { hide_tab(app, &prev)?; } }
182
- emit_snapshot(app)?; Ok(id)
183
- }
184
-
185
- fn handle_muse_action(app: &AppHandle, raw: &str) {
186
- let rest = raw.trim_start_matches("muse-action://");
187
- let (_action, query) = rest.split_once('?').unwrap_or((rest, ""));
188
- let params = parse_query(query);
189
- let url = params.get("url").cloned().unwrap_or_default();
190
- if url.is_empty() { return; }
191
- let title = params.get("title").cloned();
192
- let source = params.get("source").cloned();
193
- let app2 = app.clone();
194
- tauri::async_runtime::spawn(async move {
195
- match crate::library::library_add_item(app2.clone(), url.clone(), source, title).await {
196
- Ok(item) => {
197
- let _ = crate::board::board_add_image(app2.clone(), Some(item.id.clone()), item.data_url.clone(), 120.0, 120.0, 300.0, 200.0);
198
- let _ = app2.emit("board://image_added", serde_json::json!({"id": item.id}));
199
- }
200
- Err(e) => eprintln!("[muse-action] capture failed: {e}"),
201
- }
202
- });
203
- }
204
-
205
- fn parse_query(query: &str) -> HashMap<String, String> {
206
- query.split('&').filter_map(|pair| { let (k, v) = pair.split_once('=')?; Some((percent_decode(k), percent_decode(v))) }).collect()
207
- }
208
-
209
- fn percent_decode(s: &str) -> String {
210
- let bytes = s.as_bytes();
211
- let mut out = Vec::with_capacity(bytes.len());
212
- let mut i = 0;
213
- while i < bytes.len() {
214
- if bytes[i] == b'%' && i + 2 < bytes.len() {
215
- if let Ok(hex) = std::str::from_utf8(&bytes[i+1..i+3]) {
216
- if let Ok(v) = u8::from_str_radix(hex, 16) { out.push(v); i += 3; continue; }
217
- }
218
- }
219
- out.push(if bytes[i] == b'+' { b' ' } else { bytes[i] });
220
- i += 1;
221
- }
222
- String::from_utf8_lossy(&out).to_string()
223
- }
224
-
225
- 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();}})();"#;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }