openvoiceui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.env.example +104 -0
  2. package/Dockerfile +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +638 -0
  5. package/SETUP.md +360 -0
  6. package/app.py +232 -0
  7. package/auto-approve-devices.js +111 -0
  8. package/cli/index.js +372 -0
  9. package/config/__init__.py +4 -0
  10. package/config/default.yaml +43 -0
  11. package/config/flags.yaml +67 -0
  12. package/config/loader.py +203 -0
  13. package/config/providers.yaml +71 -0
  14. package/config/speech_normalization.yaml +182 -0
  15. package/config/theme.json +4 -0
  16. package/data/greetings.json +25 -0
  17. package/default-pages/ai-image-creator.html +915 -0
  18. package/default-pages/bulk-image-uploader.html +492 -0
  19. package/default-pages/desktop.html +2865 -0
  20. package/default-pages/file-explorer.html +854 -0
  21. package/default-pages/interactive-map.html +655 -0
  22. package/default-pages/style-guide.html +1005 -0
  23. package/default-pages/website-setup.html +1623 -0
  24. package/deploy/openclaw/Dockerfile +46 -0
  25. package/deploy/openvoiceui.service +30 -0
  26. package/deploy/setup-nginx.sh +50 -0
  27. package/deploy/setup-sudo.sh +306 -0
  28. package/deploy/skill-runner/Dockerfile +19 -0
  29. package/deploy/skill-runner/requirements.txt +14 -0
  30. package/deploy/skill-runner/server.py +269 -0
  31. package/deploy/supertonic/Dockerfile +22 -0
  32. package/deploy/supertonic/server.py +79 -0
  33. package/docker-compose.pinokio.yml +11 -0
  34. package/docker-compose.yml +59 -0
  35. package/greetings.json +25 -0
  36. package/index.html +65 -0
  37. package/inject-device-identity.js +142 -0
  38. package/package.json +82 -0
  39. package/profiles/default.json +114 -0
  40. package/profiles/manager.py +354 -0
  41. package/profiles/schema.json +337 -0
  42. package/prompts/voice-system-prompt.md +149 -0
  43. package/providers/__init__.py +39 -0
  44. package/providers/base.py +63 -0
  45. package/providers/llm/__init__.py +12 -0
  46. package/providers/llm/base.py +71 -0
  47. package/providers/llm/clawdbot_provider.py +112 -0
  48. package/providers/llm/zai_provider.py +115 -0
  49. package/providers/registry.py +320 -0
  50. package/providers/stt/__init__.py +12 -0
  51. package/providers/stt/base.py +58 -0
  52. package/providers/stt/webspeech_provider.py +49 -0
  53. package/providers/stt/whisper_provider.py +100 -0
  54. package/providers/tts/__init__.py +20 -0
  55. package/providers/tts/base.py +91 -0
  56. package/providers/tts/groq_provider.py +74 -0
  57. package/providers/tts/supertonic_provider.py +72 -0
  58. package/requirements.txt +38 -0
  59. package/routes/__init__.py +10 -0
  60. package/routes/admin.py +515 -0
  61. package/routes/canvas.py +1315 -0
  62. package/routes/chat.py +51 -0
  63. package/routes/conversation.py +2158 -0
  64. package/routes/elevenlabs_hybrid.py +306 -0
  65. package/routes/greetings.py +98 -0
  66. package/routes/icons.py +279 -0
  67. package/routes/image_gen.py +364 -0
  68. package/routes/instructions.py +190 -0
  69. package/routes/music.py +838 -0
  70. package/routes/onboarding.py +43 -0
  71. package/routes/pi.py +62 -0
  72. package/routes/profiles.py +215 -0
  73. package/routes/report_issue.py +68 -0
  74. package/routes/static_files.py +533 -0
  75. package/routes/suno.py +664 -0
  76. package/routes/theme.py +81 -0
  77. package/routes/transcripts.py +199 -0
  78. package/routes/vision.py +348 -0
  79. package/routes/workspace.py +288 -0
  80. package/server.py +1510 -0
  81. package/services/__init__.py +1 -0
  82. package/services/auth.py +143 -0
  83. package/services/canvas_versioning.py +239 -0
  84. package/services/db_pool.py +107 -0
  85. package/services/gateway.py +16 -0
  86. package/services/gateway_manager.py +333 -0
  87. package/services/gateways/__init__.py +12 -0
  88. package/services/gateways/base.py +110 -0
  89. package/services/gateways/compat.py +264 -0
  90. package/services/gateways/openclaw.py +1134 -0
  91. package/services/health.py +100 -0
  92. package/services/memory_client.py +455 -0
  93. package/services/paths.py +26 -0
  94. package/services/speech_normalizer.py +285 -0
  95. package/services/tts.py +270 -0
  96. package/setup-config.js +262 -0
  97. package/sounds/air_horn.mp3 +0 -0
  98. package/sounds/bruh.mp3 +0 -0
  99. package/sounds/crowd_cheer.mp3 +0 -0
  100. package/sounds/gunshot.mp3 +0 -0
  101. package/sounds/impact.mp3 +0 -0
  102. package/sounds/lets_go.mp3 +0 -0
  103. package/sounds/record_stop.mp3 +0 -0
  104. package/sounds/rewind.mp3 +0 -0
  105. package/sounds/sad_trombone.mp3 +0 -0
  106. package/sounds/scratch_long.mp3 +0 -0
  107. package/sounds/yeah.mp3 +0 -0
  108. package/src/adapters/ClawdBotAdapter.js +264 -0
  109. package/src/adapters/_template.js +133 -0
  110. package/src/adapters/elevenlabs-classic.js +841 -0
  111. package/src/adapters/elevenlabs-hybrid.js +812 -0
  112. package/src/adapters/hume-evi.js +676 -0
  113. package/src/admin.html +1339 -0
  114. package/src/app.js +8802 -0
  115. package/src/core/Config.js +173 -0
  116. package/src/core/EmotionEngine.js +307 -0
  117. package/src/core/EventBridge.js +180 -0
  118. package/src/core/EventBus.js +117 -0
  119. package/src/core/VoiceSession.js +607 -0
  120. package/src/face/BaseFace.js +259 -0
  121. package/src/face/EyeFace.js +208 -0
  122. package/src/face/HaloSmokeFace.js +509 -0
  123. package/src/face/manifest.json +27 -0
  124. package/src/face/previews/eyes.svg +16 -0
  125. package/src/face/previews/orb.svg +29 -0
  126. package/src/features/MusicPlayer.js +620 -0
  127. package/src/features/Soundboard.js +128 -0
  128. package/src/providers/DeepgramSTT.js +472 -0
  129. package/src/providers/DeepgramStreamingSTT.js +766 -0
  130. package/src/providers/GroqSTT.js +559 -0
  131. package/src/providers/TTSPlayer.js +323 -0
  132. package/src/providers/WebSpeechSTT.js +479 -0
  133. package/src/providers/tts/BaseTTSProvider.js +81 -0
  134. package/src/providers/tts/HumeProvider.js +77 -0
  135. package/src/providers/tts/SupertonicProvider.js +174 -0
  136. package/src/providers/tts/index.js +140 -0
  137. package/src/shell/adapter-registry.js +154 -0
  138. package/src/shell/caller-bridge.js +35 -0
  139. package/src/shell/camera-bridge.js +28 -0
  140. package/src/shell/canvas-bridge.js +32 -0
  141. package/src/shell/commercial-bridge.js +44 -0
  142. package/src/shell/face-bridge.js +44 -0
  143. package/src/shell/music-bridge.js +60 -0
  144. package/src/shell/orchestrator.js +233 -0
  145. package/src/shell/profile-discovery.js +303 -0
  146. package/src/shell/sounds-bridge.js +28 -0
  147. package/src/shell/transcript-bridge.js +61 -0
  148. package/src/shell/waveform-bridge.js +33 -0
  149. package/src/styles/base.css +2862 -0
  150. package/src/styles/face.css +417 -0
  151. package/src/styles/pi-overrides.css +89 -0
  152. package/src/styles/theme-dark.css +67 -0
  153. package/src/test-tts.html +175 -0
  154. package/src/ui/AppShell.js +544 -0
  155. package/src/ui/ProfileSwitcher.js +228 -0
  156. package/src/ui/SessionControl.js +240 -0
  157. package/src/ui/face/FacePicker.js +195 -0
  158. package/src/ui/face/FaceRenderer.js +309 -0
  159. package/src/ui/settings/PlaylistEditor.js +366 -0
  160. package/src/ui/settings/SettingsPanel.css +684 -0
  161. package/src/ui/settings/SettingsPanel.js +419 -0
  162. package/src/ui/settings/TTSVoicePreview.js +210 -0
  163. package/src/ui/themes/ThemeManager.js +213 -0
  164. package/src/ui/visualizers/BaseVisualizer.js +29 -0
  165. package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
  166. package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
  167. package/static/emulators/jsdos/js-dos.css +1 -0
  168. package/static/emulators/jsdos/js-dos.js +22 -0
  169. package/static/favicon.svg +55 -0
  170. package/static/icons/apple-touch-icon.png +0 -0
  171. package/static/icons/favicon-32.png +0 -0
  172. package/static/icons/icon-192.png +0 -0
  173. package/static/icons/icon-512.png +0 -0
  174. package/static/install.html +449 -0
  175. package/static/manifest.json +26 -0
  176. package/static/sw.js +21 -0
  177. package/tts_providers/__init__.py +136 -0
  178. package/tts_providers/base_provider.py +319 -0
  179. package/tts_providers/groq_provider.py +155 -0
  180. package/tts_providers/hume_provider.py +226 -0
  181. package/tts_providers/providers_config.json +119 -0
  182. package/tts_providers/qwen3_provider.py +371 -0
  183. package/tts_providers/resemble_provider.py +315 -0
  184. package/tts_providers/supertonic_provider.py +557 -0
  185. package/tts_providers/supertonic_tts.py +399 -0
@@ -0,0 +1,2865 @@
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>Desktop</title>
7
+ <style>
8
+ /* ══════════════════════════════════════════════════════════════
9
+ BASE STYLES
10
+ ══════════════════════════════════════════════════════════════ */
11
+ *{margin:0;padding:0;box-sizing:border-box}
12
+ html,body{width:100%!important;height:100%!important;overflow:hidden!important;background:#111!important;font-family:Tahoma,sans-serif!important;padding:0!important;color:#fff!important}
13
+
14
+ #os-container{
15
+ position:absolute;
16
+ top:40px;left:40px;right:40px;bottom:40px;
17
+ border-radius:12px;
18
+ overflow:hidden;
19
+ box-shadow:0 0 40px rgba(0,0,0,0.6);
20
+ }
21
+
22
+ #app{width:100%;height:100%;position:relative}
23
+
24
+ #desktop{
25
+ width:100%;height:100%;position:relative;
26
+ user-select:none;-webkit-user-select:none;
27
+ overflow:hidden;
28
+ }
29
+
30
+ /* ── Desktop Icons ── */
31
+ .desktop-icon{
32
+ position:absolute;
33
+ width:75px;
34
+ display:flex;flex-direction:column;align-items:center;
35
+ cursor:pointer;padding:4px;border-radius:4px;
36
+ gap:2px;
37
+ }
38
+ .desktop-icon:hover{background:rgba(255,255,255,0.1)}
39
+ .desktop-icon.selected{background:rgba(51,105,197,0.4);outline:1px dotted rgba(255,255,255,0.6)}
40
+ .desktop-icon .icon-img{width:48px;height:48px;pointer-events:none}
41
+ .desktop-icon .icon-img svg{width:100%;height:100%}
42
+ .desktop-icon .icon-label{
43
+ font-size:11px;text-align:center;
44
+ color:var(--icon-text,#fff);
45
+ text-shadow:var(--icon-shadow,1px 1px 2px rgba(0,0,0,0.9));
46
+ word-wrap:break-word;max-width:72px;
47
+ line-height:1.2;margin-top:1px;
48
+ pointer-events:none;
49
+ }
50
+
51
+ /* ── Taskbar ── */
52
+ #taskbar{
53
+ position:absolute;bottom:0;left:0;right:0;
54
+ height:var(--taskbar-h,30px);
55
+ display:flex;align-items:center;
56
+ z-index:9000;
57
+ }
58
+ #start-btn{
59
+ border:none;cursor:pointer;display:flex;align-items:center;gap:4px;
60
+ height:100%;padding:0 10px;
61
+ }
62
+ .win-flag{display:inline-grid;grid-template-columns:1fr 1fr;width:14px;height:14px;gap:1px}
63
+ .win-flag span{border-radius:1px}
64
+ #taskbar-windows{flex:1;display:flex;align-items:center;padding:0 4px;gap:2px;overflow:hidden}
65
+ .taskbar-btn{
66
+ height:22px;border:none;cursor:pointer;
67
+ font-size:11px;padding:0 8px;max-width:160px;
68
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
69
+ text-align:left;display:flex;align-items:center;gap:4px;
70
+ }
71
+ .taskbar-btn.active{font-weight:bold}
72
+ #tray{display:flex;align-items:center;gap:6px;padding:0 8px;font-size:11px;height:100%}
73
+ #clock{white-space:nowrap}
74
+
75
+ /* ── Windows ── */
76
+ .window{
77
+ position:absolute;
78
+ min-width:300px;min-height:200px;
79
+ display:flex;flex-direction:column;
80
+ z-index:100;
81
+ }
82
+ .window.minimized{display:none!important}
83
+ .window.maximized{
84
+ left:0!important;top:0!important;
85
+ width:100%!important;
86
+ border-radius:0!important;
87
+ }
88
+ .titlebar{
89
+ display:flex;align-items:center;
90
+ cursor:move;flex-shrink:0;
91
+ padding:0 4px;gap:4px;
92
+ }
93
+ .titlebar-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;pointer-events:none}
94
+ .titlebar-btns{display:flex;gap:2px;flex-shrink:0}
95
+ .tb-btn{
96
+ border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;
97
+ font-size:10px;line-height:1;padding:0;
98
+ }
99
+ .win-body{flex:1;overflow:auto;position:relative}
100
+
101
+ /* ── Window Resize Handles ── */
102
+ .win-resize{position:absolute;z-index:2}
103
+ .win-resize.n{top:-3px;left:6px;right:6px;height:6px;cursor:n-resize}
104
+ .win-resize.s{bottom:-3px;left:6px;right:6px;height:6px;cursor:s-resize}
105
+ .win-resize.e{right:-3px;top:6px;bottom:6px;width:6px;cursor:e-resize}
106
+ .win-resize.w{left:-3px;top:6px;bottom:6px;width:6px;cursor:w-resize}
107
+ .win-resize.nw{top:-3px;left:-3px;width:10px;height:10px;cursor:nw-resize}
108
+ .win-resize.ne{top:-3px;right:-3px;width:10px;height:10px;cursor:ne-resize}
109
+ .win-resize.sw{bottom:-3px;left:-3px;width:10px;height:10px;cursor:sw-resize}
110
+ .win-resize.se{bottom:-3px;right:-3px;width:10px;height:10px;cursor:se-resize}
111
+
112
+ /* ── Context Menu ── */
113
+ #context-menu{
114
+ position:fixed;z-index:10000;
115
+ min-width:180px;
116
+ display:none;
117
+ }
118
+ #context-menu.show{display:block}
119
+ .ctx-item{
120
+ padding:4px 24px 4px 28px;cursor:pointer;
121
+ font-size:12px;white-space:nowrap;position:relative;
122
+ }
123
+ .ctx-separator{margin:3px 0;border:none}
124
+ .ctx-item.has-sub::after{content:'\25B6';position:absolute;right:8px;font-size:8px}
125
+ .ctx-submenu{
126
+ position:absolute;left:100%;top:-2px;
127
+ min-width:160px;display:none;
128
+ }
129
+ .ctx-item:hover>.ctx-submenu{display:block}
130
+
131
+ /* ── Start Menu ── */
132
+ #start-menu{
133
+ position:absolute;z-index:9500;
134
+ display:none;
135
+ max-height:calc(100% - 40px);
136
+ overflow:hidden;
137
+ display:none;
138
+ }
139
+ #start-menu.show{display:flex;flex-direction:column}
140
+ .sm-header{padding:8px 12px;display:flex;align-items:center;gap:8px;flex-shrink:0}
141
+ .sm-avatar{width:40px;height:40px;border-radius:6px;background:#ddd;border:2px solid rgba(255,255,255,0.3)}
142
+ .sm-user{font-weight:bold;font-size:13px}
143
+ .sm-body{display:flex;flex:1;overflow:hidden;min-height:0}
144
+ .sm-left,.sm-right{padding:4px 0;overflow-y:auto}
145
+ .sm-item{
146
+ padding:5px 12px;cursor:pointer;
147
+ font-size:12px;display:flex;align-items:center;gap:8px;
148
+ white-space:nowrap;
149
+ }
150
+ .sm-item .sm-icon{width:24px;height:24px;flex-shrink:0}
151
+ .sm-item .sm-icon svg{width:100%;height:100%}
152
+ .sm-separator{margin:2px 0;border:none}
153
+ .sm-footer{padding:4px 0;display:flex;justify-content:flex-end;gap:4px;padding-right:8px;flex-shrink:0}
154
+ .sm-footer-btn{font-size:12px;border:none;cursor:pointer;padding:4px 12px;display:flex;align-items:center;gap:4px}
155
+
156
+ /* ── macOS Menu Bar ── */
157
+ #mac-menubar{
158
+ position:absolute;top:0;left:0;right:0;
159
+ height:25px;z-index:9000;
160
+ display:none;align-items:center;
161
+ padding:0 8px;gap:16px;
162
+ font-size:13px;font-weight:500;
163
+ }
164
+ #mac-menubar .apple-logo{font-size:16px;cursor:pointer;opacity:0.9}
165
+ #mac-menubar .menu-item{cursor:pointer;opacity:0.85}
166
+ #mac-menubar .menu-item:hover{opacity:1}
167
+ #mac-menubar .menu-right{margin-left:auto;display:flex;gap:12px;font-size:12px;font-weight:400}
168
+
169
+ /* ── macOS Dock ── */
170
+ #dock{
171
+ position:absolute;bottom:4px;left:50%;transform:translateX(-50%);
172
+ display:none;align-items:flex-end;
173
+ padding:4px 8px;gap:2px;border-radius:16px;
174
+ z-index:9000;
175
+ }
176
+ #dock .dock-icon{
177
+ width:48px;height:48px;cursor:pointer;
178
+ transition:transform 0.15s;
179
+ display:flex;flex-direction:column;align-items:center;
180
+ position:relative;
181
+ }
182
+ #dock .dock-icon:hover{transform:scale(1.3) translateY(-8px)}
183
+ #dock .dock-icon svg{width:44px;height:44px}
184
+ #dock .dock-icon .dock-dot{
185
+ width:4px;height:4px;border-radius:50%;
186
+ background:rgba(255,255,255,0.6);
187
+ position:absolute;bottom:-2px;
188
+ display:none;
189
+ }
190
+ #dock .dock-icon.has-window .dock-dot{display:block}
191
+ #dock .dock-sep{width:1px;height:44px;background:rgba(255,255,255,0.2);margin:0 4px;align-self:center}
192
+
193
+ /* ── Explorer Content ── */
194
+ .explorer-sidebar{
195
+ width:180px;border-right:1px solid #ccc;
196
+ overflow-y:auto;flex-shrink:0;
197
+ padding:4px 0;
198
+ }
199
+ .explorer-sidebar .cat-item{
200
+ padding:5px 12px;cursor:pointer;
201
+ font-size:11px;display:flex;align-items:center;gap:6px;
202
+ }
203
+ .explorer-sidebar .cat-item:hover{background:rgba(0,0,0,0.05)}
204
+ .explorer-sidebar .cat-item.active{background:rgba(51,105,197,0.15);font-weight:bold}
205
+ .explorer-content{flex:1;padding:8px;overflow-y:auto}
206
+ .explorer-grid{
207
+ display:flex;flex-wrap:wrap;gap:4px;
208
+ align-content:flex-start;
209
+ }
210
+ .explorer-item{
211
+ width:80px;padding:6px 4px;
212
+ display:flex;flex-direction:column;align-items:center;
213
+ cursor:pointer;border-radius:4px;gap:2px;
214
+ }
215
+ .explorer-item:hover{background:rgba(51,105,197,0.1)}
216
+ .explorer-item.selected{background:rgba(51,105,197,0.25);outline:1px dotted #316ac5}
217
+ .explorer-item .ei-icon{width:32px;height:32px}
218
+ .explorer-item .ei-icon svg{width:100%;height:100%}
219
+ .explorer-item .ei-name{font-size:10px;text-align:center;word-wrap:break-word;max-width:76px;line-height:1.2}
220
+ .explorer-addr{
221
+ padding:3px 8px;border-bottom:1px solid #ccc;
222
+ font-size:11px;display:flex;align-items:center;gap:4px;
223
+ background:rgba(255,255,255,0.6);
224
+ }
225
+ .explorer-addr span{font-weight:bold}
226
+ .explorer-toolbar{
227
+ padding:2px 4px;border-bottom:1px solid #ccc;
228
+ display:flex;gap:2px;
229
+ }
230
+ .explorer-toolbar button{
231
+ font-size:11px;border:none;cursor:pointer;
232
+ padding:2px 6px;background:transparent;
233
+ }
234
+ .explorer-toolbar button:hover{background:rgba(0,0,0,0.08)}
235
+
236
+ /* ── Recycle Bin Content ── */
237
+ .recycle-empty{text-align:center;padding:40px;color:#888;font-size:14px}
238
+ .recycle-toolbar{padding:4px 8px;border-bottom:1px solid #ccc;display:flex;gap:8px}
239
+ .recycle-toolbar button{font-size:11px;border:1px solid #999;padding:2px 10px;cursor:pointer;background:#f0f0f0;border-radius:2px}
240
+ .recycle-toolbar button:hover{background:#e0e0e0}
241
+
242
+ /* ── Theme Settings Window ── */
243
+ .theme-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;padding:16px}
244
+ .theme-card{
245
+ border:2px solid #ccc;border-radius:8px;cursor:pointer;
246
+ overflow:hidden;transition:border-color 0.2s;
247
+ }
248
+ .theme-card:hover{border-color:#666}
249
+ .theme-card.active{border-color:#3399ff;box-shadow:0 0 0 2px rgba(51,153,255,0.3)}
250
+ .theme-card .preview{height:80px;position:relative}
251
+ .theme-card .label{padding:6px;text-align:center;font-size:12px;font-weight:bold;background:#f5f5f5}
252
+
253
+ /* Icon size overrides */
254
+ .icons-small .desktop-icon .icon-img{width:24px!important;height:24px!important}
255
+ .icons-small .desktop-icon{width:60px!important}
256
+ .icons-small .desktop-icon .icon-label{font-size:9px!important;max-width:58px!important}
257
+
258
+ /* Disabled context item */
259
+ .ctx-item.disabled{opacity:0.4;pointer-events:none}
260
+ /* Checkmark in context item */
261
+ .ctx-check::before{content:'\2713';position:absolute;left:10px;font-size:11px}
262
+
263
+ /* Animations */
264
+ @keyframes windowOpen{from{transform:scale(0.8);opacity:0}to{transform:scale(1);opacity:1}}
265
+ .window{animation:windowOpen 0.15s ease-out}
266
+
267
+ /* ── Modal Dialog (replaces prompt/confirm — works inside iframes) ── */
268
+ #modal-overlay{
269
+ position:fixed;inset:0;z-index:2147483647;
270
+ background:rgba(0,0,0,0.55);
271
+ display:none;align-items:center;justify-content:center;
272
+ }
273
+ #modal-overlay.show{display:flex!important}
274
+ #modal-box{
275
+ background:#fff;border-radius:6px;padding:20px;
276
+ min-width:280px;max-width:380px;width:90%;
277
+ box-shadow:0 8px 30px rgba(0,0,0,0.5);
278
+ font-size:13px;color:#222;
279
+ }
280
+ .theme-95 #modal-box,.theme-31 #modal-box{
281
+ background:#c0c0c0;border:2px outset #c0c0c0;border-radius:0;
282
+ }
283
+ .theme-mac #modal-box,.theme-ubuntu #modal-box{
284
+ background:#f0f0f0;border-radius:10px;box-shadow:0 10px 40px rgba(0,0,0,0.5);
285
+ }
286
+ #modal-message{margin:0 0 10px;line-height:1.4;font-size:13px}
287
+ #modal-input{
288
+ width:100%;padding:5px 8px;border:1px solid #aaa;border-radius:3px;
289
+ font-size:13px;box-sizing:border-box;margin-bottom:2px;
290
+ }
291
+ .modal-btns{margin-top:14px;display:flex;justify-content:flex-end;gap:8px}
292
+ .modal-btn{
293
+ padding:4px 16px;border:1px solid #aaa;border-radius:3px;
294
+ cursor:pointer;font-size:12px;background:#f0f0f0;
295
+ }
296
+ .modal-btn:hover{background:#e0e0e0}
297
+ .modal-btn.primary{background:#316ac5;color:#fff;border-color:#316ac5}
298
+ .modal-btn.primary:hover{background:#2558b0}
299
+ .theme-95 .modal-btn,.theme-31 .modal-btn{border:2px outset #c0c0c0;background:#c0c0c0;border-radius:0}
300
+ .theme-95 .modal-btn:active,.theme-31 .modal-btn:active{border-style:inset}
301
+ .theme-95 .modal-btn.primary,.theme-31 .modal-btn.primary{background:#c0c0c0;color:#000;border:2px outset #c0c0c0}
302
+
303
+ /* ── Wallpaper preview ── */
304
+ .wallpaper-section{padding:12px 16px;border-top:1px solid #ddd;font-size:12px}
305
+ .theme-95 .wallpaper-section,.theme-31 .wallpaper-section{border-top:1px solid #808080}
306
+ .wallpaper-section label{font-weight:bold;display:block;margin-bottom:8px}
307
+ .wallpaper-row{display:flex;align-items:center;gap:10px}
308
+ .wallpaper-thumb{
309
+ width:80px;height:50px;border:1px solid #ccc;border-radius:3px;
310
+ object-fit:cover;flex-shrink:0;
311
+ }
312
+ .wallpaper-empty{
313
+ width:80px;height:50px;border:1px dashed #aaa;border-radius:3px;
314
+ display:flex;align-items:center;justify-content:center;
315
+ color:#999;font-size:10px;flex-shrink:0;text-align:center;
316
+ }
317
+ .wallpaper-btns{display:flex;flex-direction:column;gap:6px}
318
+ .wallpaper-btn{
319
+ padding:4px 10px;cursor:pointer;font-size:11px;
320
+ border:1px solid #aaa;border-radius:3px;background:#f0f0f0;
321
+ white-space:nowrap;
322
+ }
323
+ .wallpaper-btn:hover{background:#e0e0e0}
324
+ .theme-95 .wallpaper-btn,.theme-31 .wallpaper-btn{border:2px outset #c0c0c0;background:#c0c0c0;border-radius:0}
325
+
326
+ /* ── Drop target highlight ── */
327
+ .desktop-icon.drop-target{
328
+ background:rgba(51,153,255,0.35)!important;
329
+ outline:2px dashed rgba(51,153,255,0.8)!important;
330
+ border-radius:6px;
331
+ }
332
+
333
+ /* ── Mobile / small container responsive ── */
334
+ @media (max-width:600px) {
335
+ /* Desktop icons — smaller grid */
336
+ .desktop-icon{width:56px!important}
337
+ .desktop-icon .icon-img{width:32px!important;height:32px!important}
338
+ .desktop-icon .icon-label{font-size:9px!important;max-width:54px!important}
339
+ /* Windows — fit to screen */
340
+ .window{min-width:180px!important;min-height:120px!important}
341
+ .titlebar{height:20px!important;padding:0 3px!important}
342
+ .titlebar-title{font-size:10px!important}
343
+ .tb-btn{width:16px!important;height:16px!important;font-size:8px!important}
344
+ .win-body{font-size:11px!important}
345
+ /* Taskbar — compact */
346
+ #taskbar{height:24px!important}
347
+ #start-btn{font-size:10px!important;padding:0 6px!important}
348
+ #start-btn .win-flag{width:10px!important;height:10px!important}
349
+ .taskbar-btn{height:18px!important;font-size:9px!important;padding:0 4px!important}
350
+ #tray{font-size:9px!important;padding:0 4px!important}
351
+ /* Start menu — proportionate */
352
+ .theme-xp #start-menu{width:280px!important;bottom:24px!important}
353
+ .theme-xp .sm-right{width:120px!important}
354
+ .theme-95 #start-menu{width:170px!important;bottom:24px!important}
355
+ .sm-header{padding:4px 8px!important}
356
+ .sm-avatar{width:28px!important;height:28px!important}
357
+ .sm-user{font-size:11px!important}
358
+ .sm-item{font-size:10px!important;padding:3px 8px!important}
359
+ .sm-item .sm-icon{width:16px!important;height:16px!important}
360
+ .sm-footer-btn{font-size:10px!important;padding:3px 8px!important}
361
+ /* macOS menu bar */
362
+ #mac-menubar{height:20px!important;font-size:11px!important}
363
+ /* Explorer — compact */
364
+ .explorer-sidebar{width:100px!important}
365
+ .explorer-sidebar .cat-item{font-size:9px!important;padding:3px 6px!important}
366
+ .explorer-item{width:56px!important}
367
+ .explorer-item .ei-icon{width:24px!important;height:24px!important}
368
+ .explorer-item .ei-name{font-size:8px!important}
369
+ .explorer-toolbar button{font-size:9px!important}
370
+ .explorer-addr{font-size:9px!important}
371
+ /* Context menu */
372
+ .ctx-item{font-size:11px!important;padding:3px 16px 3px 20px!important}
373
+ /* Theme grid */
374
+ .theme-grid{grid-template-columns:1fr 1fr!important;gap:6px!important;padding:8px!important}
375
+ .theme-card .preview{height:50px!important}
376
+ .theme-card .label{font-size:10px!important;padding:4px!important}
377
+ /* Dock */
378
+ #dock .dock-icon{width:32px!important;height:32px!important}
379
+ #dock .dock-icon svg{width:28px!important;height:28px!important}
380
+ #dock{padding:2px 4px!important;gap:1px!important;border-radius:10px!important}
381
+ }
382
+ @media (max-width:400px) {
383
+ .desktop-icon{width:48px!important}
384
+ .desktop-icon .icon-img{width:26px!important;height:26px!important}
385
+ .desktop-icon .icon-label{font-size:8px!important;max-width:46px!important}
386
+ .theme-xp #start-menu{width:220px!important}
387
+ .theme-xp .sm-right{display:none!important}
388
+ .theme-95 #start-menu{width:150px!important}
389
+ .explorer-sidebar{display:none!important}
390
+ #dock{display:none!important}
391
+ }
392
+
393
+ /* ══════════════════════════════════════════════════════════════
394
+ WINDOWS XP THEME
395
+ ══════════════════════════════════════════════════════════════ */
396
+ .theme-xp #desktop{
397
+ background:
398
+ radial-gradient(ellipse 250px 80px at 15% 22%,rgba(255,255,255,0.8) 0%,transparent 70%),
399
+ radial-gradient(ellipse 350px 100px at 55% 15%,rgba(255,255,255,0.7) 0%,transparent 70%),
400
+ radial-gradient(ellipse 150px 50px at 82% 25%,rgba(255,255,255,0.65) 0%,transparent 70%),
401
+ radial-gradient(ellipse 130% 60% at 50% 135%,#4aad31 0%,#55bd3e 25%,transparent 55%),
402
+ radial-gradient(ellipse 80% 40% at 20% 115%,#3a9928 0%,#49ab34 20%,transparent 40%),
403
+ radial-gradient(ellipse 70% 35% at 80% 118%,#42a52e 0%,#52b53e 20%,transparent 35%),
404
+ radial-gradient(ellipse 60% 25% at 40% 100%,#55a845 0%,transparent 25%),
405
+ linear-gradient(180deg,#1b58d0 0%,#2568dc 10%,#3a82e6 25%,#5a9eec 40%,#7db8f0 55%,#9acff5 65%,#b5e0f8 75%,#d4eefb 85%);
406
+ }
407
+ .theme-xp{--icon-text:#fff;--icon-shadow:1px 1px 2px rgba(0,0,0,0.9);--taskbar-h:30px}
408
+ .theme-xp #mac-menubar,.theme-xp #dock{display:none!important}
409
+ .theme-xp #taskbar{
410
+ display:flex;
411
+ background:linear-gradient(180deg,#3168d5 0%,#4e8ee9 3%,#2157d7 6%,#2663e0 14%,#1941a5 20%,#1941a5 80%,#1c44a9 86%,#1b3c97 90%,#163295 95%,#122b86 100%);
412
+ border-top:1px solid #5c8fee;
413
+ }
414
+ .theme-xp #start-btn{
415
+ background:linear-gradient(180deg,#3d9c38 0%,#3ea83a 8%,#4dbb49 15%,#3fa83b 50%,#3b9d37 85%,#2d8a29 100%);
416
+ border-radius:0 8px 8px 0;color:#fff;
417
+ font:italic bold 13px 'Franklin Gothic Medium',Tahoma,sans-serif;
418
+ text-shadow:1px 1px 1px rgba(0,0,0,0.4);
419
+ letter-spacing:.5px;padding:0 14px 0 6px;
420
+ }
421
+ .theme-xp #start-btn:hover{filter:brightness(1.1)}
422
+ .theme-xp #start-btn:active{filter:brightness(0.95)}
423
+ .theme-xp .taskbar-btn{
424
+ background:linear-gradient(180deg,#3b7be3 0%,#2f6cd3 50%,#245dc3 100%);
425
+ color:#fff;border:1px solid #1a3f85;border-radius:2px;
426
+ font-size:11px;
427
+ }
428
+ .theme-xp .taskbar-btn.active{
429
+ background:linear-gradient(180deg,#1c4599 0%,#1a3f89 50%,#163579 100%);
430
+ border:1px inset #102a5e;
431
+ }
432
+ .theme-xp #tray{
433
+ background:linear-gradient(180deg,#0f8ced 0%,#0c6cc3 50%,#0c5caa 100%);
434
+ border-left:2px solid #1561a9;color:#fff;
435
+ }
436
+ .theme-xp .window{
437
+ border:3px solid #0055ea;border-radius:8px;
438
+ box-shadow:2px 2px 10px rgba(0,0,0,0.3);
439
+ }
440
+ .theme-xp .titlebar{
441
+ background:linear-gradient(180deg,#0058ee 0%,#3593ff 4%,#2b71d3 8%,#0058ee 50%,#0046d5 92%,#0036af 100%);
442
+ height:25px;border-radius:5px 5px 0 0;color:#fff;
443
+ font-weight:bold;font-size:12px;
444
+ text-shadow:1px 1px 1px rgba(0,0,0,0.3);
445
+ padding:0 4px 0 6px;
446
+ }
447
+ .theme-xp .window.inactive .titlebar{
448
+ background:linear-gradient(180deg,#7a96df 0%,#97aede 50%,#7a96df 100%);
449
+ }
450
+ .theme-xp .win-body{background:#fff;color:#000}
451
+ .theme-xp .tb-btn{width:21px;height:21px;border-radius:3px;border:1px solid rgba(0,0,0,0.3)}
452
+ .theme-xp .tb-btn.close{
453
+ background:linear-gradient(180deg,#e07f55 0%,#d1503e 50%,#c03a2a 100%);
454
+ color:#fff;font-weight:bold;
455
+ }
456
+ .theme-xp .tb-btn.close:hover{background:linear-gradient(180deg,#f09070 0%,#e05540 50%,#d04030 100%)}
457
+ .theme-xp .tb-btn.min,.theme-xp .tb-btn.max{
458
+ background:linear-gradient(180deg,#3a7bea 0%,#2965d5 50%,#1850c0 100%);
459
+ color:#fff;
460
+ }
461
+ .theme-xp .tb-btn.min:hover,.theme-xp .tb-btn.max:hover{
462
+ background:linear-gradient(180deg,#5090f0 0%,#3a7bea 50%,#2965d5 100%);
463
+ }
464
+ .theme-xp #context-menu{
465
+ background:#fff;border:1px solid #868686;
466
+ box-shadow:2px 2px 4px rgba(0,0,0,0.25);border-radius:2px;
467
+ padding:2px 0;
468
+ }
469
+ .theme-xp .ctx-item{color:#000}
470
+ .theme-xp .ctx-item:hover{background:#316ac5;color:#fff}
471
+ .theme-xp .ctx-separator{height:1px;background:#ccc}
472
+ .theme-xp .ctx-submenu{background:#fff;border:1px solid #868686;box-shadow:2px 2px 4px rgba(0,0,0,0.25);padding:2px 0}
473
+ .theme-xp #start-menu{
474
+ bottom:30px;left:0;width:380px;
475
+ background:#fff;border:2px solid #0055ea;border-radius:6px 6px 0 0;
476
+ box-shadow:2px 2px 8px rgba(0,0,0,0.4);
477
+ }
478
+ .theme-xp .sm-header{
479
+ background:linear-gradient(180deg,#0058ee 0%,#3593ff 4%,#0058ee 50%,#0036af 100%);
480
+ color:#fff;border-radius:4px 4px 0 0;
481
+ }
482
+ .theme-xp .sm-body{border-top:1px solid #ccc}
483
+ .theme-xp .sm-left{flex:1;background:#fff;border-right:1px solid #ddd;color:#000}
484
+ .theme-xp .sm-right{width:170px;background:#d3e5fa;color:#000}
485
+ .theme-xp .sm-item{color:#000}
486
+ .theme-xp .sm-item:hover{background:rgba(49,106,197,0.15)}
487
+ .theme-xp .sm-right .sm-item:hover{background:rgba(49,106,197,0.25)}
488
+ .theme-xp .sm-footer{
489
+ background:linear-gradient(180deg,#3168d5 0%,#1941a5 100%);
490
+ border-radius:0 0 4px 4px;
491
+ }
492
+ .theme-xp .sm-footer-btn{background:transparent;color:#fff;border-radius:3px}
493
+ .theme-xp .sm-footer-btn:hover{background:rgba(255,255,255,0.2)}
494
+ .theme-xp .sm-separator{height:1px;background:#ddd}
495
+ .theme-xp .explorer-sidebar{background:#d3e5fa}
496
+ .theme-xp .explorer-toolbar{background:#f1f1f1}
497
+ .theme-xp .window.maximized{height:calc(100% - 30px)!important}
498
+ .theme-xp .window.maximized .titlebar{border-radius:0}
499
+
500
+ /* ══════════════════════════════════════════════════════════════
501
+ macOS THEME
502
+ ══════════════════════════════════════════════════════════════ */
503
+ .theme-mac #desktop{
504
+ background:
505
+ radial-gradient(ellipse at 25% 40%,#5c2d82 0%,transparent 50%),
506
+ radial-gradient(ellipse at 75% 40%,#1a5276 0%,transparent 50%),
507
+ radial-gradient(ellipse at 50% 80%,#1a3c5e 0%,transparent 60%),
508
+ radial-gradient(ellipse at 50% 10%,#2c1654 0%,transparent 40%),
509
+ linear-gradient(180deg,#0d1117 0%,#1a1a3e 30%,#1a2a4e 60%,#0f1a3e 100%);
510
+ font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;
511
+ }
512
+ .theme-mac{--icon-text:#fff;--icon-shadow:0 1px 3px rgba(0,0,0,0.7);--taskbar-h:0px}
513
+ .theme-mac #taskbar{display:none!important}
514
+ .theme-mac #mac-menubar{
515
+ display:flex;
516
+ background:rgba(38,38,38,0.75);
517
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
518
+ border-bottom:1px solid rgba(255,255,255,0.1);
519
+ color:rgba(255,255,255,0.9);
520
+ font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;
521
+ }
522
+ .theme-mac #dock{
523
+ display:flex;
524
+ background:rgba(255,255,255,0.12);
525
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
526
+ border:1px solid rgba(255,255,255,0.15);
527
+ }
528
+ .theme-mac .window{
529
+ border-radius:10px;
530
+ box-shadow:0 8px 30px rgba(0,0,0,0.35),0 2px 8px rgba(0,0,0,0.2);
531
+ border:0.5px solid rgba(0,0,0,0.2);
532
+ }
533
+ .theme-mac .titlebar{
534
+ background:linear-gradient(180deg,#e8e6e3 0%,#d6d2ce 100%);
535
+ height:28px;border-radius:10px 10px 0 0;
536
+ padding:0 12px;
537
+ border-bottom:1px solid #b0aca7;
538
+ }
539
+ .theme-mac .window.inactive .titlebar{background:#f0eeeb}
540
+ .theme-mac .titlebar-title{text-align:center;color:#4a4a4a;font-size:13px;font-weight:500}
541
+ .theme-mac .titlebar-btns{order:-1;gap:6px}
542
+ .theme-mac .tb-btn{width:12px;height:12px;border-radius:50%;border:none;font-size:0}
543
+ .theme-mac .tb-btn.close{background:#ff5f57;border:0.5px solid #e33e32}
544
+ .theme-mac .tb-btn.min{background:#febc2e;border:0.5px solid #e0a016}
545
+ .theme-mac .tb-btn.max{background:#28c840;border:0.5px solid #1ea832}
546
+ .theme-mac .window:hover .tb-btn.close{background:#ff5f57}
547
+ .theme-mac .tb-btn.close:hover::after{content:'\00d7';font-size:10px;color:#4a0000;display:block;text-align:center;line-height:12px}
548
+ .theme-mac .tb-btn.min:hover::after{content:'\2013';font-size:9px;color:#6a4500;display:block;text-align:center;line-height:11px}
549
+ .theme-mac .tb-btn.max:hover::after{content:'+';font-size:11px;color:#006500;display:block;text-align:center;line-height:12px}
550
+ .theme-mac .win-body{background:#fff;color:#000}
551
+ .theme-mac #context-menu{
552
+ background:rgba(40,40,40,0.85);
553
+ backdrop-filter:blur(30px);-webkit-backdrop-filter:blur(30px);
554
+ border:0.5px solid rgba(255,255,255,0.15);border-radius:6px;
555
+ padding:4px 0;
556
+ box-shadow:0 8px 30px rgba(0,0,0,0.35);
557
+ }
558
+ .theme-mac .ctx-item{color:rgba(255,255,255,0.9);font-size:13px;padding:4px 16px;border-radius:3px;margin:0 4px}
559
+ .theme-mac .ctx-item:hover{background:#3478f6;color:#fff}
560
+ .theme-mac .ctx-separator{height:0.5px;background:rgba(255,255,255,0.15);margin:4px 8px}
561
+ .theme-mac .ctx-submenu{background:rgba(40,40,40,0.85);backdrop-filter:blur(30px);border:0.5px solid rgba(255,255,255,0.15);border-radius:6px;padding:4px 0;box-shadow:0 8px 30px rgba(0,0,0,0.35)}
562
+ .theme-mac .desktop-icon .icon-img{width:64px;height:64px}
563
+ .theme-mac .desktop-icon{width:90px}
564
+ .theme-mac .desktop-icon .icon-label{font-size:12px;max-width:88px;font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif}
565
+ .theme-mac .explorer-sidebar{background:#f5f5f5}
566
+ .theme-mac .window.maximized{height:calc(100% - 25px)!important;top:25px!important}
567
+
568
+ /* ══════════════════════════════════════════════════════════════
569
+ WINDOWS 95 THEME
570
+ ══════════════════════════════════════════════════════════════ */
571
+ .theme-95 #desktop{background:#008080}
572
+ .theme-95{--icon-text:#fff;--icon-shadow:1px 1px 1px #000;--taskbar-h:28px}
573
+ .theme-95 #mac-menubar,.theme-95 #dock{display:none!important}
574
+ .theme-95 #taskbar{
575
+ display:flex;
576
+ background:#c0c0c0;
577
+ border-top:2px solid #fff;
578
+ box-shadow:inset 0 1px 0 #dfdfdf;
579
+ }
580
+ .theme-95 #start-btn{
581
+ background:#c0c0c0;
582
+ border:2px outset #c0c0c0;
583
+ font:bold 11px 'MS Sans Serif',Tahoma,sans-serif;
584
+ color:#000;padding:0 6px;margin:2px;
585
+ height:22px;
586
+ }
587
+ .theme-95 #start-btn:active{border-style:inset}
588
+ .theme-95 .taskbar-btn{
589
+ background:#c0c0c0;border:2px outset #c0c0c0;
590
+ font-size:11px;color:#000;margin:2px 0;
591
+ }
592
+ .theme-95 .taskbar-btn.active{border-style:inset;background:#b0b0b0}
593
+ .theme-95 #tray{
594
+ border:2px inset #c0c0c0;margin:2px;
595
+ background:#c0c0c0;color:#000;font-size:11px;
596
+ }
597
+ .theme-95 .window{
598
+ background:#c0c0c0;border:2px outset #c0c0c0;
599
+ box-shadow:1px 1px 0 #000;
600
+ }
601
+ .theme-95 .titlebar{
602
+ background:#000080;height:20px;padding:0 2px;
603
+ color:#fff;font-size:11px;font-weight:bold;
604
+ }
605
+ .theme-95 .window.inactive .titlebar{background:#808080}
606
+ .theme-95 .win-body{background:#fff;color:#000;border:2px inset #c0c0c0;margin:2px}
607
+ .theme-95 .tb-btn{
608
+ width:16px;height:14px;background:#c0c0c0;
609
+ border:2px outset #c0c0c0;font-size:8px;color:#000;
610
+ }
611
+ .theme-95 .tb-btn:active{border-style:inset}
612
+ .theme-95 .tb-btn.close{font-weight:bold}
613
+ .theme-95 #context-menu{
614
+ background:#c0c0c0;border:2px outset #c0c0c0;
615
+ padding:2px 0;
616
+ }
617
+ .theme-95 .ctx-item{color:#000;font-size:11px}
618
+ .theme-95 .ctx-item:hover{background:#000080;color:#fff}
619
+ .theme-95 .ctx-separator{height:1px;border-top:1px solid #808080;border-bottom:1px solid #fff}
620
+ .theme-95 .ctx-submenu{background:#c0c0c0;border:2px outset #c0c0c0;padding:2px 0}
621
+ .theme-95 #start-menu{
622
+ bottom:28px;left:0;width:200px;
623
+ background:#c0c0c0;border:2px outset #c0c0c0;
624
+ box-shadow:2px 2px 0 #000;
625
+ }
626
+ .theme-95 .sm-header{display:none}
627
+ .theme-95 .sm-body{flex-direction:column}
628
+ .theme-95 .sm-left{border-right:none;background:#c0c0c0;border-left:22px solid #808080}
629
+ .theme-95 .sm-right{display:none}
630
+ .theme-95 .sm-item{font-size:11px;color:#000}
631
+ .theme-95 .sm-item:hover{background:#000080;color:#fff}
632
+ .theme-95 .sm-separator{height:1px;border-top:1px solid #808080;border-bottom:1px solid #fff}
633
+ .theme-95 .sm-footer{background:#c0c0c0;border-top:1px solid #808080}
634
+ .theme-95 .sm-footer-btn{background:#c0c0c0;border:2px outset #c0c0c0;color:#000;font-size:11px}
635
+ .theme-95 .sm-footer-btn:active{border-style:inset}
636
+ .theme-95 .desktop-icon .icon-img{width:32px;height:32px}
637
+ .theme-95 .desktop-icon{width:68px}
638
+ .theme-95 .desktop-icon .icon-label{font-size:10px;max-width:66px}
639
+ .theme-95 .explorer-sidebar{background:#c0c0c0;border-right:2px solid #808080}
640
+ .theme-95 .explorer-toolbar{background:#c0c0c0;border-bottom:2px solid #808080}
641
+ .theme-95 .explorer-toolbar button{font-size:10px}
642
+ .theme-95 .window.maximized{height:calc(100% - 28px)!important}
643
+ .theme-95 .window.maximized .titlebar{border-radius:0}
644
+
645
+ /* ══════════════════════════════════════════════════════════════
646
+ WINDOWS 3.1 THEME
647
+ ══════════════════════════════════════════════════════════════ */
648
+ .theme-31 #desktop{background:#00807f}
649
+ .theme-31{--icon-text:#fff;--icon-shadow:1px 1px 1px #000;--taskbar-h:0px}
650
+ .theme-31 #taskbar{display:none!important}
651
+ .theme-31 #dock{display:none!important}
652
+ .theme-31 #mac-menubar{
653
+ display:flex;
654
+ background:#c0c0c0;
655
+ border-bottom:2px solid #808080;
656
+ color:#000;font-size:12px;
657
+ font-family:'Fixedsys','Courier New',monospace;
658
+ height:22px;
659
+ }
660
+ .theme-31 #mac-menubar .apple-logo{display:none}
661
+ .theme-31 #mac-menubar .menu-item{font-weight:normal;color:#000;opacity:1}
662
+ .theme-31 #mac-menubar .menu-item:hover{background:#000080;color:#fff}
663
+ .theme-31 #mac-menubar .menu-right{color:#000}
664
+ .theme-31 .window{
665
+ background:#c0c0c0;border:2px solid #000;
666
+ box-shadow:2px 2px 0 #000;
667
+ }
668
+ .theme-31 .titlebar{
669
+ background:#000080;height:20px;padding:0 2px;
670
+ color:#fff;font-size:12px;font-weight:bold;
671
+ font-family:'Fixedsys','Courier New',monospace;
672
+ }
673
+ .theme-31 .window.inactive .titlebar{background:#808080}
674
+ .theme-31 .win-body{background:#fff;color:#000;border:1px solid #000;margin:2px}
675
+ .theme-31 .tb-btn{
676
+ width:18px;height:16px;background:#c0c0c0;
677
+ border:2px outset #c0c0c0;font-size:9px;color:#000;
678
+ font-family:'Fixedsys','Courier New',monospace;
679
+ }
680
+ .theme-31 #context-menu{
681
+ background:#c0c0c0;border:2px solid #000;padding:2px 0;
682
+ font-family:'Fixedsys','Courier New',monospace;
683
+ }
684
+ .theme-31 .ctx-item{color:#000;font-size:12px}
685
+ .theme-31 .ctx-item:hover{background:#000080;color:#fff}
686
+ .theme-31 .ctx-separator{height:1px;border-top:1px solid #808080;border-bottom:1px solid #fff}
687
+ .theme-31 .ctx-submenu{background:#c0c0c0;border:2px solid #000;padding:2px 0}
688
+ .theme-31 .desktop-icon .icon-img{width:32px;height:32px}
689
+ .theme-31 .desktop-icon{width:68px}
690
+ .theme-31 .desktop-icon .icon-label{font-size:10px;max-width:66px;font-family:'Fixedsys','Courier New',monospace}
691
+ .theme-31 .explorer-sidebar{background:#c0c0c0;border-right:1px solid #000}
692
+ .theme-31 .explorer-toolbar{background:#c0c0c0;border-bottom:1px solid #000}
693
+ .theme-31 .window.maximized{height:calc(100% - 22px)!important;top:22px!important}
694
+
695
+ /* ══════════════════════════════════════════════════════════════
696
+ UBUNTU THEME
697
+ ══════════════════════════════════════════════════════════════ */
698
+ .theme-ubuntu #desktop{
699
+ background:
700
+ radial-gradient(ellipse at 30% 60%,#77216f 0%,transparent 50%),
701
+ radial-gradient(ellipse at 70% 30%,#5e2750 0%,transparent 50%),
702
+ radial-gradient(ellipse at 50% 80%,#2c001e 0%,transparent 60%),
703
+ radial-gradient(ellipse at 20% 20%,#e95420 0%,transparent 30%),
704
+ linear-gradient(180deg,#2c001e 0%,#44133a 30%,#5e2750 50%,#2c001e 100%);
705
+ font-family:'Ubuntu','Segoe UI',sans-serif;
706
+ }
707
+ .theme-ubuntu{--icon-text:#fff;--icon-shadow:0 1px 3px rgba(0,0,0,0.7);--taskbar-h:0px}
708
+ .theme-ubuntu #taskbar{display:none!important}
709
+ .theme-ubuntu #mac-menubar{
710
+ display:flex;
711
+ background:rgba(44,0,30,0.85);
712
+ backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
713
+ border-bottom:1px solid rgba(255,255,255,0.08);
714
+ color:rgba(255,255,255,0.85);
715
+ font-family:'Ubuntu','Segoe UI',sans-serif;
716
+ font-size:13px;
717
+ }
718
+ .theme-ubuntu #mac-menubar .apple-logo{display:none}
719
+ .theme-ubuntu #mac-menubar .menu-item:first-of-type{font-weight:bold}
720
+ .theme-ubuntu #dock{
721
+ display:flex;
722
+ background:rgba(44,0,30,0.7);
723
+ backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
724
+ border:1px solid rgba(255,255,255,0.1);
725
+ border-radius:12px;
726
+ }
727
+ .theme-ubuntu .window{
728
+ border-radius:8px;
729
+ box-shadow:0 6px 24px rgba(0,0,0,0.4);
730
+ border:1px solid rgba(255,255,255,0.08);
731
+ }
732
+ .theme-ubuntu .titlebar{
733
+ background:#303030;
734
+ height:32px;border-radius:8px 8px 0 0;
735
+ padding:0 10px;
736
+ border-bottom:1px solid #222;
737
+ }
738
+ .theme-ubuntu .titlebar-title{text-align:center;color:#ddd;font-size:13px;font-weight:500}
739
+ .theme-ubuntu .window.inactive .titlebar{background:#3c3c3c}
740
+ .theme-ubuntu .win-body{background:#fff;color:#000}
741
+ .theme-ubuntu .titlebar-btns{order:-1;gap:6px}
742
+ .theme-ubuntu .tb-btn{width:14px;height:14px;border-radius:50%;border:none;font-size:0}
743
+ .theme-ubuntu .tb-btn.close{background:#e95420}
744
+ .theme-ubuntu .tb-btn.min{background:#5c616c}
745
+ .theme-ubuntu .tb-btn.max{background:#5c616c}
746
+ .theme-ubuntu .tb-btn.close:hover{background:#f07846}
747
+ .theme-ubuntu .tb-btn.min:hover{background:#7c818c}
748
+ .theme-ubuntu .tb-btn.max:hover{background:#7c818c}
749
+ .theme-ubuntu .tb-btn.close:hover::after{content:'\00d7';font-size:11px;color:#fff;display:block;text-align:center;line-height:14px}
750
+ .theme-ubuntu .tb-btn.min:hover::after{content:'\2013';font-size:9px;color:#fff;display:block;text-align:center;line-height:13px}
751
+ .theme-ubuntu .tb-btn.max:hover::after{content:'+';font-size:12px;color:#fff;display:block;text-align:center;line-height:14px}
752
+ .theme-ubuntu #context-menu{
753
+ background:rgba(48,48,48,0.95);
754
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
755
+ border:1px solid rgba(255,255,255,0.1);border-radius:8px;
756
+ padding:4px 0;
757
+ box-shadow:0 6px 24px rgba(0,0,0,0.4);
758
+ }
759
+ .theme-ubuntu .ctx-item{color:rgba(255,255,255,0.9);font-size:13px;padding:6px 16px;border-radius:4px;margin:0 4px}
760
+ .theme-ubuntu .ctx-item:hover{background:#e95420;color:#fff}
761
+ .theme-ubuntu .ctx-separator{height:0.5px;background:rgba(255,255,255,0.1);margin:4px 8px}
762
+ .theme-ubuntu .ctx-submenu{background:rgba(48,48,48,0.95);backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:4px 0;box-shadow:0 6px 24px rgba(0,0,0,0.4)}
763
+ .theme-ubuntu .desktop-icon .icon-img{width:56px;height:56px}
764
+ .theme-ubuntu .desktop-icon{width:85px}
765
+ .theme-ubuntu .desktop-icon .icon-label{font-size:11px;max-width:82px;font-family:'Ubuntu','Segoe UI',sans-serif}
766
+ .theme-ubuntu .explorer-sidebar{background:#f5f5f5}
767
+ .theme-ubuntu .window.maximized{height:calc(100% - 25px)!important;top:25px!important}
768
+ </style>
769
+ </head>
770
+ <body>
771
+ <style>html,body{padding:0!important;margin:0!important;background:#111!important;overflow:hidden!important}</style>
772
+ <div id="os-container">
773
+ <div id="app" class="theme-xp">
774
+ <div id="desktop"></div>
775
+ <div id="context-menu"></div>
776
+ <div id="start-menu"></div>
777
+ <div id="mac-menubar">
778
+ <span class="apple-logo" onclick="toggleStartMenu()">&#63743;</span>
779
+ <span class="menu-item" style="font-weight:bold">Finder</span>
780
+ <span class="menu-item">File</span>
781
+ <span class="menu-item">Edit</span>
782
+ <span class="menu-item">View</span>
783
+ <span class="menu-item">Go</span>
784
+ <span class="menu-item">Window</span>
785
+ <span class="menu-item">Help</span>
786
+ <span class="menu-right"><span id="mac-clock"></span></span>
787
+ </div>
788
+ <div id="taskbar">
789
+ <button id="start-btn" onclick="toggleStartMenu()">
790
+ <span class="win-flag"><span style="background:#ea3323"></span><span style="background:#49a942"></span><span style="background:#1e81d3"></span><span style="background:#eea621"></span></span>
791
+ <span>start</span>
792
+ </button>
793
+ <div id="taskbar-windows"></div>
794
+ <div id="tray"><span id="clock"></span></div>
795
+ </div>
796
+ <div id="dock"></div>
797
+ </div>
798
+ </div>
799
+
800
+ <!-- Modal dialog (works inside iframes, replaces prompt/confirm) -->
801
+ <div id="modal-overlay" onclick="if(event.target===this)closeModal()">
802
+ <div id="modal-box">
803
+ <div id="modal-message"></div>
804
+ <input type="text" id="modal-input" autocomplete="off">
805
+ <div class="modal-btns">
806
+ <button class="modal-btn" id="modal-cancel" onclick="closeModal()">Cancel</button>
807
+ <button class="modal-btn primary" id="modal-ok" onclick="submitModal()">OK</button>
808
+ </div>
809
+ </div>
810
+ </div>
811
+ <!-- Hidden wallpaper file picker -->
812
+ <input type="file" id="wallpaper-file-input" accept="image/*" style="display:none" onchange="handleWallpaperFile(this)">
813
+
814
+ <script>
815
+ /* ══════════════════════════════════════════════════════════════
816
+ DATA
817
+ ══════════════════════════════════════════════════════════════ */
818
+ // Populated dynamically by fetchManifest()
819
+ let CATEGORIES = {};
820
+ let PAGES = {};
821
+
822
+ /* ══════════════════════════════════════════════════════════════
823
+ SVG ICONS
824
+ ══════════════════════════════════════════════════════════════ */
825
+ const ICONS = {
826
+ computer:`<svg viewBox="0 0 48 48"><rect x="6" y="4" width="36" height="26" rx="3" fill="#2c3e50"/><rect x="8" y="6" width="32" height="22" rx="1" fill="#5dade2"/><rect x="10" y="8" width="28" height="18" fill="#3498db"/><rect x="18" y="30" width="12" height="5" fill="#95a5a6"/><rect x="12" y="35" width="24" height="3" rx="1.5" fill="#bdc3c7"/><rect x="14" y="36" width="20" height="1" fill="#a0a0a0"/></svg>`,
827
+ folder:`<svg viewBox="0 0 48 48"><rect x="4" y="16" width="40" height="26" rx="2" fill="#f5c842"/><path d="M4 16V14a2 2 0 0 1 2-2h14l4 4h18a2 2 0 0 1 2 2v2H4z" fill="#e8b84b"/><rect x="4" y="18" width="40" height="2" fill="rgba(0,0,0,0.05)"/></svg>`,
828
+ 'folder-open':`<svg viewBox="0 0 48 48"><path d="M4 14V12a2 2 0 012-2h14l4 4h18a2 2 0 012 2v2H4z" fill="#e8b84b"/><path d="M2 18h36a2 2 0 012 2l-4 22H6L2 20a2 2 0 012-2z" fill="#f5c842"/><rect x="4" y="16" width="40" height="2" fill="#e8b84b"/></svg>`,
829
+ recycle:`<svg viewBox="0 0 48 48"><rect x="14" y="6" width="20" height="4" rx="2" fill="#7f8c8d"/><rect x="20" y="4" width="8" height="4" rx="1" fill="#95a5a6"/><rect x="10" y="10" width="28" height="3" rx="1" fill="#95a5a6"/><path d="M13 13h22l-2 31H15z" fill="#bdc3c7"/><path d="M13 13h22l-1 4H14z" fill="#aab4be"/><line x1="20" y1="20" x2="19" y2="40" stroke="#95a5a6" stroke-width="1.5"/><line x1="24" y1="20" x2="24" y2="40" stroke="#95a5a6" stroke-width="1.5"/><line x1="28" y1="20" x2="29" y2="40" stroke="#95a5a6" stroke-width="1.5"/></svg>`,
830
+ 'recycle-full':`<svg viewBox="0 0 48 48"><rect x="14" y="6" width="20" height="4" rx="2" fill="#7f8c8d"/><rect x="20" y="4" width="8" height="4" rx="1" fill="#95a5a6"/><rect x="10" y="10" width="28" height="3" rx="1" fill="#95a5a6"/><path d="M13 13h22l-2 31H15z" fill="#bdc3c7"/><path d="M13 13h22l-1 4H14z" fill="#aab4be"/><rect x="17" y="15" width="5" height="8" rx="1" fill="#e8e8e8" transform="rotate(-10 19 19)"/><rect x="25" y="14" width="5" height="9" rx="1" fill="#d0d0d0" transform="rotate(5 27 18)"/><rect x="20" y="17" width="4" height="7" rx="1" fill="#f0f0f0" transform="rotate(-3 22 20)"/></svg>`,
831
+ document:`<svg viewBox="0 0 48 48"><path d="M12 4h18l10 10v30a2 2 0 01-2 2H12a2 2 0 01-2-2V6a2 2 0 012-2z" fill="#fff" stroke="#ccc" stroke-width="1"/><path d="M30 4v10h10" fill="#eee" stroke="#ccc" stroke-width="1"/><line x1="16" y1="22" x2="34" y2="22" stroke="#ddd" stroke-width="1.5"/><line x1="16" y1="28" x2="34" y2="28" stroke="#ddd" stroke-width="1.5"/><line x1="16" y1="34" x2="28" y2="34" stroke="#ddd" stroke-width="1.5"/></svg>`,
832
+ internet:`<svg viewBox="0 0 48 48"><circle cx="24" cy="24" r="20" fill="#3498db"/><circle cx="24" cy="24" r="20" fill="none" stroke="#2980b9" stroke-width="1"/><ellipse cx="24" cy="24" rx="10" ry="20" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1.5"/><line x1="4" y1="24" x2="44" y2="24" stroke="rgba(255,255,255,0.5)" stroke-width="1.5"/><ellipse cx="24" cy="15" rx="17" ry="5" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="1"/><ellipse cx="24" cy="33" rx="17" ry="5" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="1"/></svg>`,
833
+ music:`<svg viewBox="0 0 48 48"><circle cx="14" cy="36" r="6" fill="#e74c3c"/><circle cx="14" cy="36" r="2" fill="#c0392b"/><circle cx="34" cy="32" r="6" fill="#e74c3c"/><circle cx="34" cy="32" r="2" fill="#c0392b"/><rect x="18" y="8" width="3" height="28" fill="#2c3e50" rx="1"/><rect x="38" y="4" width="3" height="28" fill="#2c3e50" rx="1"/><rect x="18" y="6" width="23" height="5" fill="#2c3e50" rx="1"/></svg>`,
834
+ game:`<svg viewBox="0 0 48 48"><rect x="4" y="14" width="40" height="22" rx="11" fill="#2c3e50"/><rect x="4" y="14" width="40" height="11" rx="11" fill="#34495e"/><circle cx="14" cy="25" r="3.5" fill="#ecf0f1"/><rect x="11.5" y="23.5" width="5" height="1.5" rx=".5" fill="#bdc3c7"/><rect x="13.2" y="21.5" width="1.5" height="5" rx=".5" fill="#bdc3c7"/><circle cx="34" cy="22" r="2.5" fill="#e74c3c"/><circle cx="38" cy="26" r="2.5" fill="#3498db"/><circle cx="30" cy="26" r="2.5" fill="#2ecc71"/><circle cx="34" cy="30" r="2.5" fill="#f39c12"/></svg>`,
835
+ tools:`<svg viewBox="0 0 48 48"><path d="M30 8a10 10 0 00-9 14L8 35a4 4 0 005.5 5.5L27 27a10 10 0 0013-5 10 10 0 00-2-11l-5 5-4-1-1-4 5-5a10 10 0 00-3 1z" fill="#95a5a6" stroke="#7f8c8d" stroke-width="1"/><circle cx="10.5" cy="37.5" r="1.5" fill="#7f8c8d"/></svg>`,
836
+ book:`<svg viewBox="0 0 48 48"><rect x="8" y="6" width="32" height="36" rx="2" fill="#3498db"/><rect x="8" y="6" width="6" height="36" rx="2" fill="#2980b9"/><rect x="14" y="6" width="26" height="36" rx="1" fill="#ecf0f1"/><line x1="18" y1="14" x2="36" y2="14" stroke="#bdc3c7" stroke-width="1.5"/><line x1="18" y1="20" x2="36" y2="20" stroke="#bdc3c7" stroke-width="1.5"/><line x1="18" y1="26" x2="30" y2="26" stroke="#bdc3c7" stroke-width="1.5"/></svg>`,
837
+ settings:`<svg viewBox="0 0 48 48"><circle cx="24" cy="24" r="7" fill="none" stroke="#7f8c8d" stroke-width="4"/><circle cx="24" cy="24" r="3" fill="#7f8c8d"/><g stroke="#7f8c8d" stroke-width="4" stroke-linecap="round"><line x1="24" y1="4" x2="24" y2="11"/><line x1="24" y1="37" x2="24" y2="44"/><line x1="4" y1="24" x2="11" y2="24"/><line x1="37" y1="24" x2="44" y2="24"/><line x1="9.9" y1="9.9" x2="14.8" y2="14.8"/><line x1="33.2" y1="33.2" x2="38.1" y2="38.1"/><line x1="9.9" y1="38.1" x2="14.8" y2="33.2"/><line x1="33.2" y1="14.8" x2="38.1" y2="9.9"/></g></svg>`,
838
+ };
839
+
840
+ function getIcon(type) { return ICONS[type] || ICONS.document; }
841
+
842
+ /* ══════════════════════════════════════════════════════════════
843
+ STATE — persisted server-side via manifest API
844
+ ══════════════════════════════════════════════════════════════ */
845
+ let currentTheme = 'xp';
846
+ let windows = new Map();
847
+ let nextWinId = 1;
848
+ let nextZ = 100;
849
+ let recycleBin = [];
850
+ let iconPositions = {};
851
+ let dragState = null;
852
+ let selectedIcons = new Set();
853
+ let startMenuOpen = false;
854
+ let contextMenuOpen = false;
855
+ let _saveTimer = null;
856
+ let customFolders = {}; // { 'custom-xyz': { name:'Folder', pages:['p1','p2'] } }
857
+ let desktopPages = []; // page IDs shown as loose desktop icons
858
+ let knownPages = []; // all page IDs we've ever seen
859
+ let _manifestTimer = null;
860
+ let wallpaperUrl = null; // server URL of custom desktop wallpaper
861
+ let savedPageCategories = {}; // { pageId: catId } — persists drag-to-folder assignments across fetchManifest cycles
862
+
863
+ // Save state to server (debounced)
864
+ function saveState() {
865
+ clearTimeout(_saveTimer);
866
+ _saveTimer = setTimeout(_doSaveState, 500);
867
+ }
868
+ function _doSaveState() {
869
+ const state = JSON.stringify({
870
+ theme: currentTheme, iconPositions, recycleBin,
871
+ customFolders, desktopPages, knownPages, wallpaperUrl, savedPageCategories,
872
+ });
873
+ fetch('/api/canvas/manifest/page/desktop', {
874
+ method: 'PATCH',
875
+ headers: {'Content-Type': 'application/json'},
876
+ body: JSON.stringify({description: state}),
877
+ }).catch(() => {});
878
+ }
879
+
880
+ // Load state from server
881
+ async function loadState() {
882
+ try {
883
+ const resp = await fetch('/api/canvas/manifest/page/desktop');
884
+ if (!resp.ok) return;
885
+ const data = await resp.json();
886
+ if (data.description) {
887
+ try {
888
+ const state = JSON.parse(data.description);
889
+ if (state.theme) currentTheme = state.theme;
890
+ if (state.iconPositions) iconPositions = state.iconPositions;
891
+ if (state.recycleBin) recycleBin = state.recycleBin;
892
+ if (state.customFolders) customFolders = state.customFolders;
893
+ if (state.desktopPages) desktopPages = state.desktopPages;
894
+ if (state.knownPages) knownPages = state.knownPages;
895
+ if (state.wallpaperUrl) { wallpaperUrl = state.wallpaperUrl; applyWallpaper(wallpaperUrl); }
896
+ if (state.savedPageCategories) savedPageCategories = state.savedPageCategories;
897
+ } catch(e) {}
898
+ }
899
+ } catch(e) {}
900
+ }
901
+
902
+ /* ══════════════════════════════════════════════════════════════
903
+ MANIFEST SYNC — fetch pages dynamically from server
904
+ ══════════════════════════════════════════════════════════════ */
905
+ async function fetchManifest(forceSync) {
906
+ try {
907
+ const url = '/api/canvas/manifest' + (forceSync ? '?sync=1' : '');
908
+ const resp = await fetch(url);
909
+ if (!resp.ok) { console.warn('[desktop] manifest fetch failed:', resp.status); return; }
910
+ const data = await resp.json();
911
+
912
+ // Categories to hide from desktop (system UI, empty, etc.)
913
+ const HIDDEN_CATS = ['system','test-dev','uncategorized'];
914
+
915
+ // Rebuild CATEGORIES from manifest
916
+ const newCats = {};
917
+ if (data.categories) {
918
+ Object.keys(data.categories).forEach(catId => {
919
+ if (HIDDEN_CATS.includes(catId)) return;
920
+ const mc = data.categories[catId];
921
+ // Skip empty categories with no pages
922
+ if (!mc.pages || mc.pages.length === 0) return;
923
+ const iconMap = {'ai-office':'folder','dashboard-mockups':'folder','tts-voice':'music',
924
+ 'openclaw-reviews':'folder','onboarding':'folder','api-developer':'tools',
925
+ 'openvoiceui':'folder','entertainment':'game','reference':'book',
926
+ 'business-tools':'tools','jambot-platform':'folder','uncategorized':'folder'};
927
+ newCats[catId] = {
928
+ name: mc.name || catId,
929
+ icon: iconMap[catId] || 'folder',
930
+ color: mc.color || '#6b7280',
931
+ };
932
+ });
933
+ }
934
+ CATEGORIES = newCats;
935
+
936
+ // Rebuild PAGES from manifest — include ALL user-visible pages
937
+ const HIDDEN_PAGES = ['desktop','home','inbox','create','office'];
938
+ const PAGE_HIDDEN_CATS = ['system','test-dev'];
939
+ const newPages = {};
940
+ if (data.pages) {
941
+ Object.keys(data.pages).forEach(pageId => {
942
+ if (HIDDEN_PAGES.includes(pageId)) return;
943
+ const mp = data.pages[pageId];
944
+ if (PAGE_HIDDEN_CATS.includes(mp.category)) return;
945
+ newPages[pageId] = {
946
+ name: mp.display_name || pageId,
947
+ cat: mp.category || 'uncategorized',
948
+ iconUrl: (mp.icon && /^(\/|https?:)/.test(mp.icon)) ? mp.icon : null,
949
+ };
950
+ });
951
+ }
952
+ PAGES = newPages;
953
+
954
+ // Apply saved category overrides (drag-to-folder assignments survive fetchManifest)
955
+ Object.keys(savedPageCategories).forEach(pageId => {
956
+ if (PAGES[pageId]) PAGES[pageId].cat = savedPageCategories[pageId];
957
+ });
958
+
959
+ // Detect new pages
960
+ const currentIds = Object.keys(PAGES);
961
+ if (knownPages.length === 0) {
962
+ // First run — seed knownPages without flooding desktop
963
+ knownPages = currentIds.slice();
964
+ saveState();
965
+ } else {
966
+ const newFound = currentIds.filter(id => !knownPages.includes(id));
967
+ if (newFound.length > 0) {
968
+ newFound.forEach(id => {
969
+ desktopPages.push(id);
970
+ knownPages.push(id);
971
+ });
972
+ saveState();
973
+ }
974
+ }
975
+
976
+ rebuildDesktopItems();
977
+ buildDesktopIcons();
978
+ } catch(e) { console.warn('[desktop] fetchManifest error:', e); }
979
+ }
980
+
981
+ /* ══════════════════════════════════════════════════════════════
982
+ THEME
983
+ ══════════════════════════════════════════════════════════════ */
984
+ function setTheme(theme) {
985
+ currentTheme = theme;
986
+ saveState();
987
+ document.getElementById('app').className = 'theme-' + theme;
988
+ updateClock();
989
+ buildDesktopIcons();
990
+ buildDock();
991
+ updateTaskbar();
992
+ closeAllMenus();
993
+ if (wallpaperUrl) applyWallpaper(wallpaperUrl); // re-apply after class change
994
+ // Update maximized windows height
995
+ windows.forEach((w) => {
996
+ if (w.el.classList.contains('maximized')) {
997
+ let h = getDesktopHeight();
998
+ w.el.style.height = h + 'px';
999
+ }
1000
+ });
1001
+ }
1002
+
1003
+ function getDesktopHeight() {
1004
+ const c = document.getElementById('os-container');
1005
+ const th = currentTheme;
1006
+ if (th === 'xp') return c.offsetHeight - 30;
1007
+ if (th === '95') return c.offsetHeight - 28;
1008
+ if (th === 'mac' || th === 'ubuntu') return c.offsetHeight - 25;
1009
+ if (th === '31') return c.offsetHeight - 22;
1010
+ return c.offsetHeight;
1011
+ }
1012
+ function getDesktopTop() {
1013
+ if (currentTheme === 'mac' || currentTheme === 'ubuntu') return 25;
1014
+ if (currentTheme === '31') return 22;
1015
+ return 0;
1016
+ }
1017
+
1018
+ /* ══════════════════════════════════════════════════════════════
1019
+ CLOCK
1020
+ ══════════════════════════════════════════════════════════════ */
1021
+ function updateClock() {
1022
+ const now = new Date();
1023
+ const t = now.toLocaleTimeString('en-US', {hour:'numeric',minute:'2-digit',hour12:true});
1024
+ const el = document.getElementById('clock');
1025
+ if (el) el.textContent = t;
1026
+ const mel = document.getElementById('mac-clock');
1027
+ if (mel) {
1028
+ const d = now.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'});
1029
+ mel.textContent = d + ' ' + t;
1030
+ }
1031
+ }
1032
+ setInterval(updateClock, 30000);
1033
+
1034
+ /* ══════════════════════════════════════════════════════════════
1035
+ DESKTOP ICONS
1036
+ ══════════════════════════════════════════════════════════════ */
1037
+ const SYSTEM_ITEMS = [
1038
+ {id:'my-computer',name:'My Computer',icon:'computer',action:'explorer',macName:'Macintosh HD',w31Name:'File Manager',ubuntuName:'Files'},
1039
+ {id:'recycle-bin',name:'Recycle Bin',icon:'recycle',action:'recycle',macName:'Trash',ubuntuName:'Trash'},
1040
+ {id:'my-documents',name:'My Documents',icon:'folder',action:'docs',ubuntuName:'Documents'},
1041
+ {id:'internet',name:'Internet',icon:'internet',action:'internet',ubuntuName:'Web Browser'},
1042
+ {id:'settings',name:'Display Properties',icon:'settings',action:'settings',macName:'System Preferences',w31Name:'Control Panel',ubuntuName:'Settings'},
1043
+ {id:'game-library-shortcut',name:'Game Library',icon:'game',action:'openPage',pageId:'game-library',macName:'Game Library',ubuntuName:'Games'},
1044
+ ];
1045
+ let DESKTOP_ITEMS = [];
1046
+
1047
+ function rebuildDesktopItems() {
1048
+ DESKTOP_ITEMS = SYSTEM_ITEMS.slice();
1049
+ // Category folders
1050
+ Object.keys(CATEGORIES).forEach(catId => {
1051
+ DESKTOP_ITEMS.push({
1052
+ id: 'cat-' + catId,
1053
+ name: CATEGORIES[catId].name,
1054
+ icon: CATEGORIES[catId].icon === 'folder' ? 'folder' : CATEGORIES[catId].icon,
1055
+ action: 'folder',
1056
+ category: catId,
1057
+ });
1058
+ });
1059
+ // Custom folders
1060
+ Object.keys(customFolders).forEach(fid => {
1061
+ if (DESKTOP_ITEMS.find(i => i.id === fid)) return;
1062
+ DESKTOP_ITEMS.push({
1063
+ id: fid,
1064
+ name: customFolders[fid].name,
1065
+ icon: 'folder',
1066
+ action: 'customFolder',
1067
+ folderId: fid,
1068
+ });
1069
+ });
1070
+ // Loose desktop page icons (new/unassigned pages)
1071
+ desktopPages.forEach(pageId => {
1072
+ if (!PAGES[pageId]) return;
1073
+ if (DESKTOP_ITEMS.find(i => i.id === 'page-' + pageId)) return;
1074
+ DESKTOP_ITEMS.push({
1075
+ id: 'page-' + pageId,
1076
+ name: PAGES[pageId].name,
1077
+ icon: getPageIconType(PAGES[pageId].cat, pageId, PAGES[pageId].name),
1078
+ iconUrl: PAGES[pageId].iconUrl || null,
1079
+ action: 'openPage',
1080
+ pageId: pageId,
1081
+ });
1082
+ });
1083
+ }
1084
+ rebuildDesktopItems();
1085
+
1086
+ function getIconName(item) {
1087
+ if (currentTheme === 'mac' && item.macName) return item.macName;
1088
+ if (currentTheme === 'ubuntu' && item.ubuntuName) return item.ubuntuName;
1089
+ if (currentTheme === '31' && item.w31Name) return item.w31Name;
1090
+ return item.name;
1091
+ }
1092
+
1093
+ function buildDesktopIcons() {
1094
+ const desktop = document.getElementById('desktop');
1095
+ desktop.querySelectorAll('.desktop-icon').forEach(el => el.remove());
1096
+ const isRightToLeft = currentTheme === 'mac';
1097
+ const container = document.getElementById('os-container');
1098
+ const isSmall = container && container.offsetWidth < 500;
1099
+ const startX = isRightToLeft ? desktop.offsetWidth - (isSmall ? 65 : 90) : 10;
1100
+ const startY = getDesktopTop() + 10;
1101
+ const colH = getDesktopHeight() - 20;
1102
+ const cellW = isSmall ? 60 : (currentTheme === 'mac' ? 95 : 80);
1103
+ const cellH = isSmall ? 60 : (currentTheme === 'mac' ? 95 : 80);
1104
+ const maxPerCol = Math.max(1, Math.floor(colH / cellH));
1105
+
1106
+ DESKTOP_ITEMS.forEach((item, i) => {
1107
+ if (recycleBin.includes(item.id) && item.action !== 'recycle') return;
1108
+ const col = Math.floor(i / maxPerCol);
1109
+ const row = i % maxPerCol;
1110
+ const saved = iconPositions[item.id];
1111
+ const x = saved ? saved.x : (isRightToLeft ? startX - col * cellW : startX + col * cellW);
1112
+ const y = saved ? saved.y : startY + row * cellH;
1113
+
1114
+ const el = document.createElement('div');
1115
+ el.className = 'desktop-icon';
1116
+ el.dataset.id = item.id;
1117
+ el.style.left = x + 'px';
1118
+ el.style.top = y + 'px';
1119
+
1120
+ const recycleIcon = item.id === 'recycle-bin'
1121
+ ? (recycleBin.length > 0 ? 'recycle-full' : 'recycle')
1122
+ : item.icon;
1123
+
1124
+ const iconHtml = item.iconUrl
1125
+ ? `<img src="${item.iconUrl}" style="width:100%;height:100%;object-fit:contain;">`
1126
+ : getIcon(recycleIcon);
1127
+ el.innerHTML = `<div class="icon-img">${iconHtml}</div><div class="icon-label">${getIconName(item)}</div>`;
1128
+ el.addEventListener('mousedown', (e) => onIconMouseDown(e, item));
1129
+ el.addEventListener('touchstart', (e) => onIconTouchStart(e, item), {passive:false});
1130
+ el.addEventListener('dblclick', (e) => { e.stopPropagation(); onIconDblClick(item); });
1131
+ desktop.appendChild(el);
1132
+ });
1133
+ }
1134
+
1135
+ function onIconTouchStart(e, item) {
1136
+ e.preventDefault();
1137
+ const touch = e.touches[0];
1138
+ const el = e.currentTarget;
1139
+ const startX = touch.clientX, startY = touch.clientY;
1140
+ const origLeft = parseInt(el.style.left), origTop = parseInt(el.style.top);
1141
+ let moved = false;
1142
+ let lastDropTarget = null;
1143
+ let tapTimer = setTimeout(() => { moved = true; }, 300); // long press enables drag
1144
+
1145
+ function onMove(ev) {
1146
+ const t = ev.touches[0];
1147
+ const dx = t.clientX - startX, dy = t.clientY - startY;
1148
+ if (!moved && Math.abs(dx) < 10 && Math.abs(dy) < 10) return;
1149
+ moved = true;
1150
+ clearTimeout(tapTimer);
1151
+ ev.preventDefault();
1152
+ el.style.left = (origLeft + dx) + 'px';
1153
+ el.style.top = (origTop + dy) + 'px';
1154
+ const target = findDropTarget(el, item);
1155
+ if (target !== lastDropTarget) {
1156
+ if (lastDropTarget) lastDropTarget.classList.remove('drop-target');
1157
+ if (target) target.classList.add('drop-target');
1158
+ lastDropTarget = target;
1159
+ }
1160
+ }
1161
+ function onEnd() {
1162
+ clearTimeout(tapTimer);
1163
+ document.removeEventListener('touchmove', onMove);
1164
+ document.removeEventListener('touchend', onEnd);
1165
+ if (lastDropTarget) lastDropTarget.classList.remove('drop-target');
1166
+ if (moved) {
1167
+ const target = findDropTarget(el, item);
1168
+ if (target) {
1169
+ const targetItem = DESKTOP_ITEMS.find(i => i.id === target.dataset.id);
1170
+ if (targetItem && targetItem.action === 'recycle') {
1171
+ sendToRecycle(item);
1172
+ } else {
1173
+ dropIntoFolder(item, target.dataset.id);
1174
+ }
1175
+ el.style.left = origLeft + 'px';
1176
+ el.style.top = origTop + 'px';
1177
+ } else {
1178
+ iconPositions[item.id] = {x: parseInt(el.style.left), y: parseInt(el.style.top)};
1179
+ saveState();
1180
+ }
1181
+ } else {
1182
+ onIconDblClick(item);
1183
+ }
1184
+ }
1185
+ document.addEventListener('touchmove', onMove, {passive:false});
1186
+ document.addEventListener('touchend', onEnd);
1187
+ }
1188
+
1189
+ function onIconMouseDown(e, item) {
1190
+ if (e.button !== 0) return;
1191
+ if (iconsLocked) { e.stopPropagation(); return; }
1192
+ e.stopPropagation();
1193
+ closeAllMenus();
1194
+ if (!e.ctrlKey) {
1195
+ selectedIcons.clear();
1196
+ }
1197
+ selectedIcons.add(item.id);
1198
+ updateIconSelection();
1199
+
1200
+ const el = e.currentTarget;
1201
+ const startX = e.clientX, startY = e.clientY;
1202
+ const origLeft = parseInt(el.style.left), origTop = parseInt(el.style.top);
1203
+ let moved = false;
1204
+ let lastDropTarget = null;
1205
+
1206
+ function onMove(ev) {
1207
+ const dx = ev.clientX - startX, dy = ev.clientY - startY;
1208
+ if (!moved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return;
1209
+ moved = true;
1210
+ el.style.left = (origLeft + dx) + 'px';
1211
+ el.style.top = (origTop + dy) + 'px';
1212
+ // Highlight drop targets (folders)
1213
+ const target = findDropTarget(el, item);
1214
+ if (target !== lastDropTarget) {
1215
+ if (lastDropTarget) lastDropTarget.classList.remove('drop-target');
1216
+ if (target) target.classList.add('drop-target');
1217
+ lastDropTarget = target;
1218
+ }
1219
+ }
1220
+ function onUp() {
1221
+ document.removeEventListener('mousemove', onMove);
1222
+ document.removeEventListener('mouseup', onUp);
1223
+ if (lastDropTarget) lastDropTarget.classList.remove('drop-target');
1224
+ if (moved) {
1225
+ const target = findDropTarget(el, item);
1226
+ if (target) {
1227
+ const targetItem = DESKTOP_ITEMS.find(i => i.id === target.dataset.id);
1228
+ if (targetItem && targetItem.action === 'recycle') {
1229
+ sendToRecycle(item);
1230
+ } else {
1231
+ dropIntoFolder(item, target.dataset.id);
1232
+ }
1233
+ // Reset position since it's going into a folder/trash
1234
+ el.style.left = origLeft + 'px';
1235
+ el.style.top = origTop + 'px';
1236
+ } else {
1237
+ iconPositions[item.id] = {x: parseInt(el.style.left), y: parseInt(el.style.top)};
1238
+ saveState();
1239
+ }
1240
+ }
1241
+ }
1242
+ document.addEventListener('mousemove', onMove);
1243
+ document.addEventListener('mouseup', onUp);
1244
+ }
1245
+
1246
+ function findDropTarget(draggedEl, draggedItem) {
1247
+ const dragRect = draggedEl.getBoundingClientRect();
1248
+ const cx = dragRect.left + dragRect.width / 2;
1249
+ const cy = dragRect.top + dragRect.height / 2;
1250
+ const icons = document.querySelectorAll('.desktop-icon');
1251
+ for (const icon of icons) {
1252
+ if (icon === draggedEl) continue;
1253
+ const id = icon.dataset.id;
1254
+ const targetItem = DESKTOP_ITEMS.find(i => i.id === id);
1255
+ if (!targetItem) continue;
1256
+ // Folders, custom folders, and recycle bin are valid drop targets
1257
+ if (targetItem.action !== 'folder' && targetItem.action !== 'customFolder' && targetItem.action !== 'recycle') continue;
1258
+ // Don't allow dropping the recycle bin into itself
1259
+ if (targetItem.action === 'recycle' && draggedItem.action === 'recycle') continue;
1260
+ const r = icon.getBoundingClientRect();
1261
+ if (cx >= r.left && cx <= r.right && cy >= r.top && cy <= r.bottom) {
1262
+ return icon;
1263
+ }
1264
+ }
1265
+ return null;
1266
+ }
1267
+
1268
+ function dropIntoFolder(draggedItem, targetId) {
1269
+ // Determine what page/item is being dragged
1270
+ let pageId = null;
1271
+ if (draggedItem.action === 'openPage' && draggedItem.pageId) {
1272
+ pageId = draggedItem.pageId;
1273
+ } else if (draggedItem.id.startsWith('page-')) {
1274
+ pageId = draggedItem.id.replace('page-', '');
1275
+ }
1276
+ if (!pageId || !PAGES[pageId]) return;
1277
+
1278
+ const targetItem = DESKTOP_ITEMS.find(i => i.id === targetId);
1279
+ if (!targetItem) return;
1280
+
1281
+ // Move into category folder
1282
+ if (targetItem.action === 'folder' && targetItem.category) {
1283
+ PAGES[pageId].cat = targetItem.category;
1284
+ savedPageCategories[pageId] = targetItem.category; // persist across fetchManifest
1285
+ }
1286
+ // Move into custom folder
1287
+ else if (targetItem.action === 'customFolder' && targetItem.folderId) {
1288
+ if (!customFolders[targetItem.folderId]) return;
1289
+ if (!customFolders[targetItem.folderId].pages.includes(pageId)) {
1290
+ customFolders[targetItem.folderId].pages.push(pageId);
1291
+ }
1292
+ } else {
1293
+ return; // not a valid folder target — don't remove from desktop
1294
+ }
1295
+
1296
+ // Remove from desktopPages
1297
+ desktopPages = desktopPages.filter(id => id !== pageId);
1298
+ delete iconPositions[draggedItem.id];
1299
+ saveState();
1300
+ rebuildDesktopItems();
1301
+ buildDesktopIcons();
1302
+ }
1303
+
1304
+ function updateIconSelection() {
1305
+ document.querySelectorAll('.desktop-icon').forEach(el => {
1306
+ el.classList.toggle('selected', selectedIcons.has(el.dataset.id));
1307
+ });
1308
+ }
1309
+
1310
+ function onIconDblClick(item) {
1311
+ if (item.action === 'explorer') openExplorer();
1312
+ else if (item.action === 'recycle') openRecycleBin();
1313
+ else if (item.action === 'docs') openExplorer('ai-office');
1314
+ else if (item.action === 'internet') openExplorer('api-developer');
1315
+ else if (item.action === 'settings') openThemeSettings();
1316
+ else if (item.action === 'folder') openExplorer(item.category);
1317
+ else if (item.action === 'customFolder') openCustomFolder(item.folderId);
1318
+ else if (item.action === 'openPage') openCanvasPage(item.pageId);
1319
+ }
1320
+
1321
+ /* ══════════════════════════════════════════════════════════════
1322
+ WINDOW MANAGER
1323
+ ══════════════════════════════════════════════════════════════ */
1324
+ function createWindow(opts) {
1325
+ const id = nextWinId++;
1326
+ const el = document.createElement('div');
1327
+ el.className = 'window';
1328
+ el.dataset.winId = id;
1329
+ el.style.zIndex = nextZ++;
1330
+ const container = document.getElementById('os-container');
1331
+ const cw = container.offsetWidth;
1332
+ const ch = getDesktopHeight();
1333
+ const wantW = opts.width || 600;
1334
+ const wantH = opts.height || 400;
1335
+ el.style.width = Math.min(wantW, cw - 20) + 'px';
1336
+ el.style.height = Math.min(wantH, ch - 20) + 'px';
1337
+ const maxW = cw - 50;
1338
+ const maxH = ch - 50;
1339
+ const winW = parseInt(el.style.width);
1340
+ const winH = parseInt(el.style.height);
1341
+ el.style.left = Math.max(0, Math.floor((cw - winW) / 2)) + 'px';
1342
+ el.style.top = Math.max(getDesktopTop(), Math.floor(getDesktopTop() + (ch - winH) / 2)) + 'px';
1343
+
1344
+ const isMacLike = currentTheme === 'mac' || currentTheme === 'ubuntu';
1345
+ const btnSymbols = isMacLike
1346
+ ? {close:'', min:'', max:''}
1347
+ : {close:'\u00D7', min:'\u2013', max:'\u25A1'};
1348
+
1349
+ el.innerHTML = `
1350
+ <div class="titlebar" onmousedown="startWindowDrag(event,${id})">
1351
+ <div class="titlebar-btns">
1352
+ <button class="tb-btn close" onclick="closeWindow(${id})" title="Close">${btnSymbols.close}</button>
1353
+ <button class="tb-btn min" onclick="minimizeWindow(${id})" title="Minimize">${btnSymbols.min}</button>
1354
+ <button class="tb-btn max" onclick="maximizeWindow(${id})" title="Maximize">${btnSymbols.max}</button>
1355
+ </div>
1356
+ <div class="titlebar-title">${opts.title || 'Window'}</div>
1357
+ ${!isMacLike ? '<div class="titlebar-btns"><button class="tb-btn min" onclick="minimizeWindow('+id+')" title="Minimize">\u2013</button><button class="tb-btn max" onclick="maximizeWindow('+id+')" title="Maximize">\u25A1</button><button class="tb-btn close" onclick="closeWindow('+id+')" title="Close">\u00D7</button></div>' : ''}
1358
+ </div>
1359
+ <div class="win-body">${opts.content || ''}</div>
1360
+ `;
1361
+
1362
+ // For non-mac-like themes, remove the first set of buttons (left-side ones)
1363
+ if (!isMacLike) {
1364
+ const firstBtns = el.querySelector('.titlebar-btns');
1365
+ if (firstBtns) firstBtns.remove();
1366
+ } else {
1367
+ // For mac-like, remove the second set (right-side)
1368
+ const allBtns = el.querySelectorAll('.titlebar-btns');
1369
+ if (allBtns.length > 1) allBtns[1].remove();
1370
+ }
1371
+
1372
+ // Add resize handles
1373
+ ['n','s','e','w','nw','ne','sw','se'].forEach(dir => {
1374
+ const handle = document.createElement('div');
1375
+ handle.className = 'win-resize ' + dir;
1376
+ handle.addEventListener('mousedown', (ev) => startWindowResize(ev, id, dir));
1377
+ el.appendChild(handle);
1378
+ });
1379
+
1380
+ const desktop = document.getElementById('desktop');
1381
+ desktop.appendChild(el);
1382
+ el.addEventListener('mousedown', () => focusWindow(id));
1383
+
1384
+ const win = {id, el, title: opts.title, minimized: false, maximized: false, origRect: null};
1385
+ windows.set(id, win);
1386
+ focusWindow(id);
1387
+ updateTaskbar();
1388
+ buildDock();
1389
+ return id;
1390
+ }
1391
+
1392
+ function closeWindow(id) {
1393
+ const win = windows.get(id);
1394
+ if (!win) return;
1395
+ win.el.remove();
1396
+ windows.delete(id);
1397
+ updateTaskbar();
1398
+ buildDock();
1399
+ }
1400
+
1401
+ function minimizeWindow(id) {
1402
+ const win = windows.get(id);
1403
+ if (!win) return;
1404
+ win.minimized = true;
1405
+ win.el.classList.add('minimized');
1406
+ updateTaskbar();
1407
+ }
1408
+
1409
+ function maximizeWindow(id) {
1410
+ const win = windows.get(id);
1411
+ if (!win) return;
1412
+ if (win.maximized) {
1413
+ // Restore
1414
+ win.maximized = false;
1415
+ win.el.classList.remove('maximized');
1416
+ if (win.origRect) {
1417
+ win.el.style.left = win.origRect.left;
1418
+ win.el.style.top = win.origRect.top;
1419
+ win.el.style.width = win.origRect.width;
1420
+ win.el.style.height = win.origRect.height;
1421
+ }
1422
+ } else {
1423
+ win.origRect = {
1424
+ left: win.el.style.left, top: win.el.style.top,
1425
+ width: win.el.style.width, height: win.el.style.height
1426
+ };
1427
+ win.maximized = true;
1428
+ win.el.classList.add('maximized');
1429
+ const container = document.getElementById('os-container');
1430
+ win.el.style.width = container.offsetWidth + 'px';
1431
+ win.el.style.height = getDesktopHeight() + 'px';
1432
+ win.el.style.top = getDesktopTop() + 'px';
1433
+ win.el.style.left = '0px';
1434
+ }
1435
+ }
1436
+
1437
+ function focusWindow(id) {
1438
+ const win = windows.get(id);
1439
+ if (!win) return;
1440
+ if (win.minimized) {
1441
+ win.minimized = false;
1442
+ win.el.classList.remove('minimized');
1443
+ }
1444
+ win.el.style.zIndex = nextZ++;
1445
+ // Mark all inactive
1446
+ windows.forEach((w) => w.el.classList.toggle('inactive', w.id !== id));
1447
+ updateTaskbar();
1448
+ }
1449
+
1450
+ function startWindowDrag(e, id) {
1451
+ if (e.target.closest('.tb-btn')) return;
1452
+ e.preventDefault();
1453
+ const win = windows.get(id);
1454
+ if (!win || win.maximized) return;
1455
+ focusWindow(id);
1456
+ const el = win.el;
1457
+ const startX = e.clientX, startY = e.clientY;
1458
+ const origLeft = parseInt(el.style.left), origTop = parseInt(el.style.top);
1459
+ const container = document.getElementById('os-container');
1460
+
1461
+ function onMove(ev) {
1462
+ let newLeft = origLeft + ev.clientX - startX;
1463
+ let newTop = origTop + ev.clientY - startY;
1464
+ // Clamp to desktop bounds (keep at least 40px visible)
1465
+ const maxLeft = container.offsetWidth - 40;
1466
+ const maxTop = getDesktopHeight() + getDesktopTop() - 30;
1467
+ newLeft = Math.max(-el.offsetWidth + 40, Math.min(newLeft, maxLeft));
1468
+ newTop = Math.max(getDesktopTop(), Math.min(newTop, maxTop));
1469
+ el.style.left = newLeft + 'px';
1470
+ el.style.top = newTop + 'px';
1471
+ }
1472
+ function onUp() {
1473
+ document.removeEventListener('mousemove', onMove);
1474
+ document.removeEventListener('mouseup', onUp);
1475
+ }
1476
+ document.addEventListener('mousemove', onMove);
1477
+ document.addEventListener('mouseup', onUp);
1478
+ }
1479
+
1480
+ function startWindowResize(e, id, dir) {
1481
+ e.preventDefault();
1482
+ e.stopPropagation();
1483
+ const win = windows.get(id);
1484
+ if (!win || win.maximized) return;
1485
+ focusWindow(id);
1486
+ const el = win.el;
1487
+ const startX = e.clientX, startY = e.clientY;
1488
+ const origLeft = parseInt(el.style.left), origTop = parseInt(el.style.top);
1489
+ const origW = el.offsetWidth, origH = el.offsetHeight;
1490
+ const container = document.getElementById('os-container');
1491
+ const minW = 200, minH = 120;
1492
+
1493
+ function onMove(ev) {
1494
+ const dx = ev.clientX - startX, dy = ev.clientY - startY;
1495
+ let newLeft = origLeft, newTop = origTop, newW = origW, newH = origH;
1496
+ const maxW = container.offsetWidth;
1497
+ const maxH = getDesktopHeight();
1498
+ const dTop = getDesktopTop();
1499
+
1500
+ if (dir.includes('e')) newW = Math.min(Math.max(minW, origW + dx), maxW - origLeft);
1501
+ if (dir.includes('w')) {
1502
+ const dw = Math.min(dx, origW - minW);
1503
+ newLeft = Math.max(0, origLeft + dw);
1504
+ newW = origW - (newLeft - origLeft);
1505
+ if (newW + newLeft > maxW) newW = maxW - newLeft;
1506
+ }
1507
+ if (dir.includes('s')) newH = Math.min(Math.max(minH, origH + dy), maxH + dTop - origTop);
1508
+ if (dir.includes('n')) {
1509
+ const dh = Math.min(dy, origH - minH);
1510
+ newTop = Math.max(dTop, origTop + dh);
1511
+ newH = origH - (newTop - origTop);
1512
+ if (newH + newTop > maxH + dTop) newH = maxH + dTop - newTop;
1513
+ }
1514
+
1515
+ el.style.left = newLeft + 'px';
1516
+ el.style.top = newTop + 'px';
1517
+ el.style.width = newW + 'px';
1518
+ el.style.height = newH + 'px';
1519
+ }
1520
+ function onUp() {
1521
+ document.removeEventListener('mousemove', onMove);
1522
+ document.removeEventListener('mouseup', onUp);
1523
+ }
1524
+ document.addEventListener('mousemove', onMove);
1525
+ document.addEventListener('mouseup', onUp);
1526
+ }
1527
+
1528
+ /* ══════════════════════════════════════════════════════════════
1529
+ TASKBAR
1530
+ ══════════════════════════════════════════════════════════════ */
1531
+ function updateTaskbar() {
1532
+ const container = document.getElementById('taskbar-windows');
1533
+ if (!container) return;
1534
+ container.innerHTML = '';
1535
+ windows.forEach((win) => {
1536
+ const btn = document.createElement('button');
1537
+ btn.className = 'taskbar-btn' + (win.minimized ? '' : ' active');
1538
+ btn.textContent = win.title;
1539
+ btn.onclick = () => {
1540
+ if (win.minimized) focusWindow(win.id);
1541
+ else if (parseInt(win.el.style.zIndex) === nextZ - 1) minimizeWindow(win.id);
1542
+ else focusWindow(win.id);
1543
+ };
1544
+ container.appendChild(btn);
1545
+ });
1546
+ }
1547
+
1548
+ /* ══════════════════════════════════════════════════════════════
1549
+ DOCK (macOS)
1550
+ ══════════════════════════════════════════════════════════════ */
1551
+ function buildDock() {
1552
+ if (currentTheme !== 'mac' && currentTheme !== 'ubuntu') return;
1553
+ const dock = document.getElementById('dock');
1554
+ dock.innerHTML = '';
1555
+ // System icons
1556
+ const dockItems = [
1557
+ {icon:'computer',label:'Finder',action:()=>openExplorer()},
1558
+ {icon:'settings',label:'Preferences',action:()=>openThemeSettings()},
1559
+ {icon:'internet',label:'Internet',action:()=>openExplorer('api-developer')},
1560
+ {icon:'game',label:'Games',action:()=>openCanvasPage('game-library')},
1561
+ {icon:'music',label:'Music',action:()=>openExplorer('tts-voice')},
1562
+ {icon:'book',label:'Reference',action:()=>openExplorer('reference')},
1563
+ {icon:'folder',label:'Documents',action:()=>openExplorer('ai-office')},
1564
+ ];
1565
+ dockItems.forEach(item => {
1566
+ const d = document.createElement('div');
1567
+ d.className = 'dock-icon';
1568
+ d.innerHTML = `${getIcon(item.icon)}<div class="dock-dot"></div>`;
1569
+ d.title = item.label;
1570
+ d.onclick = item.action;
1571
+ dock.appendChild(d);
1572
+ });
1573
+ // Separator
1574
+ dock.innerHTML += '<div class="dock-sep"></div>';
1575
+ // Trash
1576
+ const trash = document.createElement('div');
1577
+ trash.className = 'dock-icon';
1578
+ trash.innerHTML = `${getIcon(recycleBin.length > 0 ? 'recycle-full' : 'recycle')}<div class="dock-dot"></div>`;
1579
+ trash.title = 'Trash';
1580
+ trash.onclick = () => openRecycleBin();
1581
+ dock.appendChild(trash);
1582
+ }
1583
+
1584
+ /* ══════════════════════════════════════════════════════════════
1585
+ CONTEXT MENU
1586
+ ══════════════════════════════════════════════════════════════ */
1587
+ function showContextMenu(x, y, items) {
1588
+ const menu = document.getElementById('context-menu');
1589
+ menu.innerHTML = '';
1590
+ items.forEach(item => {
1591
+ if (item === '---') {
1592
+ const sep = document.createElement('div');
1593
+ sep.className = 'ctx-separator';
1594
+ menu.appendChild(sep);
1595
+ return;
1596
+ }
1597
+ const div = document.createElement('div');
1598
+ div.className = 'ctx-item' + (item.sub ? ' has-sub' : '');
1599
+ div.textContent = item.label;
1600
+ if (item.action) {
1601
+ const act = item.action;
1602
+ div.addEventListener('mousedown', (e) => e.stopPropagation());
1603
+ div.addEventListener('click', (e) => {
1604
+ e.stopPropagation();
1605
+ closeAllMenus();
1606
+ try { act(); } catch(err) { showConfirmModal('Error: ' + err.message, ()=>{}); }
1607
+ });
1608
+ }
1609
+ if (item.sub) {
1610
+ const subMenu = document.createElement('div');
1611
+ subMenu.className = 'ctx-submenu';
1612
+ item.sub.forEach(si => {
1613
+ if (si === '---') { const sep = document.createElement('div'); sep.className = 'ctx-separator'; subMenu.appendChild(sep); return; }
1614
+ const sd = document.createElement('div');
1615
+ sd.className = 'ctx-item';
1616
+ sd.textContent = si.label;
1617
+ if (si.action) {
1618
+ const sact = si.action;
1619
+ sd.addEventListener('mousedown', (e) => e.stopPropagation());
1620
+ sd.addEventListener('click', (e) => { e.stopPropagation(); closeAllMenus(); try { sact(); } catch(err) { showConfirmModal('Error: ' + err.message, ()=>{}); } });
1621
+ }
1622
+ subMenu.appendChild(sd);
1623
+ });
1624
+ div.appendChild(subMenu);
1625
+ }
1626
+ menu.appendChild(div);
1627
+ });
1628
+
1629
+ menu.style.left = Math.max(5, Math.min(x, window.innerWidth - 210)) + 'px';
1630
+ menu.style.top = Math.max(5, Math.min(y, window.innerHeight - items.length * 28 - 10)) + 'px';
1631
+ menu.classList.add('show');
1632
+ contextMenuOpen = true;
1633
+ }
1634
+
1635
+ let clipboard = null; // {action:'cut'|'copy', id:string, pageId?:string}
1636
+ let showDesktopIcons = true;
1637
+ let iconsLocked = false;
1638
+
1639
+ function getDesktopContextItems() {
1640
+ const isMac = currentTheme === 'mac';
1641
+ const isUbuntu = currentTheme === 'ubuntu';
1642
+ const is95 = currentTheme === '95';
1643
+ const is31 = currentTheme === '31';
1644
+
1645
+ if (isMac || isUbuntu) {
1646
+ return [
1647
+ {label:'New Folder', action:()=>createNewFolder()},
1648
+ {label:'New Shortcut\u2026', action:()=>createShortcut()},
1649
+ '---',
1650
+ {label:'Sort By Name', action:()=>sortIcons('name')},
1651
+ {label:'Sort By Kind', action:()=>sortIcons('type')},
1652
+ {label:'Clean Up', action:()=>arrangeIcons()},
1653
+ '---',
1654
+ ...(clipboard ? [{label:'Paste Shortcut', action:()=>pasteItem()},'---'] : []),
1655
+ {label:(iconsLocked?'\u2713 ':'')+'Lock Icon Positions', action:()=>toggleLockIcons()},
1656
+ '---',
1657
+ {label:isMac?'Change Desktop Background\u2026':'Change Background\u2026', action:()=>openThemeSettings()},
1658
+ {label:isMac?'Get Info':'Properties', action:()=>showDesktopProperties()},
1659
+ ];
1660
+ }
1661
+
1662
+ // Windows XP / 95 / 3.1
1663
+ return [
1664
+ {label:'New Folder\u2026', action:()=>createNewFolder()},
1665
+ {label:'New Shortcut\u2026', action:()=>createShortcut()},
1666
+ '---',
1667
+ {label:(showDesktopIcons?'\u2713 ':'')+'Show Desktop Icons', action:()=>toggleDesktopIcons()},
1668
+ {label:(iconsLocked?'\u2713 ':'')+'Lock Icon Positions', action:()=>toggleLockIcons()},
1669
+ '---',
1670
+ {label:'Sort By Name', action:()=>sortIcons('name')},
1671
+ {label:'Sort By Type', action:()=>sortIcons('type')},
1672
+ {label:'Auto Arrange', action:()=>arrangeIcons()},
1673
+ '---',
1674
+ ...(clipboard ? [{label:'Paste Shortcut', action:()=>pasteItem()},'---'] : []),
1675
+ {label:'Refresh', action:()=>{rebuildDesktopItems();buildDesktopIcons();}},
1676
+ '---',
1677
+ {label:is95||is31?'Properties':(currentTheme==='xp'?'Properties':'Display Properties'), action:()=>openThemeSettings()},
1678
+ ];
1679
+ }
1680
+
1681
+ function getIconContextItems(item) {
1682
+ const isMac = currentTheme === 'mac';
1683
+ const isUbuntu = currentTheme === 'ubuntu';
1684
+
1685
+ // Recycle bin / Trash
1686
+ if (item.action === 'recycle') {
1687
+ if (isMac || isUbuntu) {
1688
+ return [
1689
+ {label:'Open', action:()=>onIconDblClick(item)},
1690
+ '---',
1691
+ {label:'Empty Trash', action:()=>{showConfirmModal('Permanently delete all items in the Trash?',(ok)=>{if(ok)emptyRecycleBin()})}},
1692
+ '---',
1693
+ {label:isMac?'Get Info':'Properties', action:()=>showProperties(item)},
1694
+ ];
1695
+ }
1696
+ return [
1697
+ {label:'Open', action:()=>onIconDblClick(item)},
1698
+ {label:'Explore', action:()=>onIconDblClick(item)},
1699
+ '---',
1700
+ {label:'Empty Recycle Bin', action:()=>{showConfirmModal('Are you sure you want to delete all items in the Recycle Bin?',(ok)=>{if(ok)emptyRecycleBin()})}},
1701
+ '---',
1702
+ {label:'Create Shortcut', action:()=>{}},
1703
+ '---',
1704
+ {label:'Properties', action:()=>showProperties(item)},
1705
+ ];
1706
+ }
1707
+
1708
+ // ── Loose page icons (check BEFORE system icon catch-all) ──
1709
+ if (item.action === 'openPage' && item.pageId) {
1710
+ const isSystem = SYSTEM_ITEMS.some(s => s.id === item.id); // e.g. game-library-shortcut
1711
+ const moveToSub = buildMoveToSubmenu(item);
1712
+ const doDelete = () => showConfirmModal('Move "'+item.name+'" to the Recycle Bin?', (ok)=>{ if(ok) sendToRecycle(item); });
1713
+ const doRename = () => showPromptModal('Rename "'+item.name+'" to:', item.name, (n)=>{ if(n&&n.trim()&&PAGES[item.pageId]){PAGES[item.pageId].name=n.trim();item.name=n.trim();rebuildDesktopItems();buildDesktopIcons();} });
1714
+ if (isMac || isUbuntu) {
1715
+ return [
1716
+ {label:'Open', action:()=>onIconDblClick(item)},
1717
+ {label:'Open in New Tab', action:()=>openCanvasPage(item.pageId, true)},
1718
+ '---',
1719
+ {label:isMac?'Get Info':'Properties', action:()=>showPageProperties(item.pageId)},
1720
+ '---',
1721
+ {label:'Rename', action:doRename},
1722
+ {label:'Move to\u2026', sub: moveToSub},
1723
+ ...(!isSystem ? [{label:isMac?'Move to Trash':'Move to Trash', action:doDelete}] : []),
1724
+ ];
1725
+ }
1726
+ return [
1727
+ {label:'Open', action:()=>onIconDblClick(item)},
1728
+ {label:'Open in New Tab', action:()=>openCanvasPage(item.pageId, true)},
1729
+ '---',
1730
+ {label:'Rename', action:doRename},
1731
+ {label:'Move to\u2026', sub: moveToSub},
1732
+ '---',
1733
+ ...(!isSystem ? [{label:'Delete', action:doDelete},'---'] : []),
1734
+ {label:'Properties', action:()=>showPageProperties(item.pageId)},
1735
+ ];
1736
+ }
1737
+
1738
+ // ── Custom folder icons ──
1739
+ if (item.action === 'customFolder') {
1740
+ const doDeleteFolder = () => showConfirmModal('Delete folder "'+item.name+'"?', (ok)=>{ if(ok){delete customFolders[item.folderId];saveState();rebuildDesktopItems();buildDesktopIcons();} });
1741
+ if (isMac || isUbuntu) {
1742
+ return [
1743
+ {label:'Open', action:()=>onIconDblClick(item)},
1744
+ {label:'Add Pages\u2026', action:()=>addPagesToFolder(item.folderId)},
1745
+ '---',
1746
+ {label:isMac?'Get Info':'Properties', action:()=>showProperties(item)},
1747
+ '---',
1748
+ {label:'Rename', action:()=>renameCustomFolder(item)},
1749
+ {label:isMac?'Move to Trash':'Move to Trash', action:doDeleteFolder},
1750
+ ];
1751
+ }
1752
+ return [
1753
+ {label:'Open', action:()=>onIconDblClick(item)},
1754
+ {label:'Add Pages\u2026', action:()=>addPagesToFolder(item.folderId)},
1755
+ '---',
1756
+ {label:'Rename', action:()=>renameCustomFolder(item)},
1757
+ {label:'Delete', action:doDeleteFolder},
1758
+ '---',
1759
+ {label:'Properties', action:()=>showProperties(item)},
1760
+ ];
1761
+ }
1762
+
1763
+ // ── System icons (My Computer, My Documents, Internet, Settings) ──
1764
+ if (!item.id.startsWith('cat-') && item.action !== 'folder') {
1765
+ if (isMac || isUbuntu) {
1766
+ return [
1767
+ {label:'Open', action:()=>onIconDblClick(item)},
1768
+ {label:isMac?'Get Info':'Properties', action:()=>showProperties(item)},
1769
+ ];
1770
+ }
1771
+ return [
1772
+ {label:'Open', action:()=>onIconDblClick(item)},
1773
+ {label:'Explore', action:()=>onIconDblClick(item)},
1774
+ '---',
1775
+ {label:'Properties', action:()=>showProperties(item)},
1776
+ ];
1777
+ }
1778
+
1779
+ // ── Category folder icons ──
1780
+ if (isMac || isUbuntu) {
1781
+ return [
1782
+ {label:'Open', action:()=>onIconDblClick(item)},
1783
+ {label:isMac?'Get Info':'Properties', action:()=>showProperties(item)},
1784
+ ];
1785
+ }
1786
+ return [
1787
+ {label:'Open', action:()=>onIconDblClick(item)},
1788
+ {label:'Explore', action:()=>onIconDblClick(item)},
1789
+ '---',
1790
+ {label:'Properties', action:()=>showProperties(item)},
1791
+ ];
1792
+ }
1793
+
1794
+ function buildMoveToSubmenu(item) {
1795
+ const subs = [];
1796
+ Object.keys(CATEGORIES).forEach(catId => {
1797
+ subs.push({
1798
+ label: CATEGORIES[catId].name,
1799
+ action: () => {
1800
+ if (item.pageId) {
1801
+ PAGES[item.pageId].cat = catId;
1802
+ savedPageCategories[item.pageId] = catId; // persist
1803
+ desktopPages = desktopPages.filter(id => id !== item.pageId);
1804
+ saveState();
1805
+ rebuildDesktopItems();
1806
+ buildDesktopIcons();
1807
+ }
1808
+ }
1809
+ });
1810
+ });
1811
+ if (Object.keys(customFolders).length > 0) {
1812
+ subs.push('---');
1813
+ Object.keys(customFolders).forEach(fid => {
1814
+ subs.push({
1815
+ label: customFolders[fid].name,
1816
+ action: () => {
1817
+ if (item.pageId && !customFolders[fid].pages.includes(item.pageId)) {
1818
+ customFolders[fid].pages.push(item.pageId);
1819
+ desktopPages = desktopPages.filter(id => id !== item.pageId);
1820
+ saveState();
1821
+ rebuildDesktopItems();
1822
+ buildDesktopIcons();
1823
+ }
1824
+ }
1825
+ });
1826
+ });
1827
+ }
1828
+ return subs;
1829
+ }
1830
+
1831
+ function renameCustomFolder(item) {
1832
+ const folder = customFolders[item.folderId];
1833
+ if (!folder) return;
1834
+ showPromptModal('Rename "' + folder.name + '" to:', folder.name, (newName) => {
1835
+ if (!newName || !newName.trim() || newName.trim() === folder.name) return;
1836
+ folder.name = newName.trim();
1837
+ saveState();
1838
+ rebuildDesktopItems();
1839
+ buildDesktopIcons();
1840
+ });
1841
+ }
1842
+
1843
+ /* ── Context menu helper actions ── */
1844
+
1845
+ function setIconSize(size) {
1846
+ // Toggle icon size via CSS class
1847
+ document.getElementById('app').classList.remove('icons-small','icons-large');
1848
+ if (size === 'small') document.getElementById('app').classList.add('icons-small');
1849
+ else document.getElementById('app').classList.add('icons-large');
1850
+ }
1851
+
1852
+ function toggleDesktopIcons() {
1853
+ showDesktopIcons = !showDesktopIcons;
1854
+ document.querySelectorAll('.desktop-icon').forEach(el => {
1855
+ el.style.display = showDesktopIcons ? '' : 'none';
1856
+ });
1857
+ }
1858
+
1859
+ function sortIcons(by) {
1860
+ iconPositions = {};
1861
+ // Sort DESKTOP_ITEMS in-place
1862
+ DESKTOP_ITEMS.sort((a, b) => {
1863
+ if (by === 'name') return getIconName(a).localeCompare(getIconName(b));
1864
+ if (by === 'type') {
1865
+ const typeOrder = {explorer:0,recycle:1,docs:2,internet:3,settings:4,folder:5};
1866
+ return (typeOrder[a.action]||9) - (typeOrder[b.action]||9);
1867
+ }
1868
+ return 0;
1869
+ });
1870
+ saveState();
1871
+ buildDesktopIcons();
1872
+ }
1873
+
1874
+ function createNewFolder() {
1875
+ showPromptModal('New folder name:', 'New Folder', (name) => {
1876
+ if (!name || !name.trim()) return;
1877
+ const id = 'custom-' + Date.now() + '-' + name.trim().toLowerCase().replace(/[^a-z0-9]+/g,'-');
1878
+ customFolders[id] = { name: name.trim(), pages: [] };
1879
+ saveState();
1880
+ rebuildDesktopItems();
1881
+ buildDesktopIcons();
1882
+ // Open the folder immediately so user can add pages
1883
+ openCustomFolder(id);
1884
+ });
1885
+ }
1886
+
1887
+ function pasteItem() {
1888
+ if (!clipboard) return;
1889
+ const {action, id, pageId} = clipboard;
1890
+ const pid = pageId || id; // support both clipboard shapes
1891
+ if (pid && PAGES[pid]) {
1892
+ if (!desktopPages.includes(pid)) desktopPages.push(pid);
1893
+ if (!knownPages.includes(pid)) knownPages.push(pid);
1894
+ saveState();
1895
+ rebuildDesktopItems();
1896
+ buildDesktopIcons();
1897
+ }
1898
+ if (action === 'cut') clipboard = null;
1899
+ }
1900
+
1901
+ function toggleLockIcons() {
1902
+ iconsLocked = !iconsLocked;
1903
+ const app = document.getElementById('app');
1904
+ app.style.cursor = iconsLocked ? 'default' : '';
1905
+ }
1906
+
1907
+ async function createShortcut() {
1908
+ // Force-sync manifest to ensure ALL pages are shown (including newly created ones)
1909
+ await fetchManifest(true);
1910
+ const available = Object.entries(PAGES).filter(([id]) =>
1911
+ !recycleBin.includes(id)
1912
+ );
1913
+ if (!available.length) { showConfirmModal('No canvas pages available.', ()=>{}); return; }
1914
+ let html = '<div style="padding:12px;font-size:12px;display:flex;flex-direction:column;height:100%">';
1915
+ html += '<div style="font-weight:bold;margin-bottom:8px">Select a page to add to desktop:</div>';
1916
+ html += '<div style="flex:1;overflow-y:auto;border:1px solid #bbb;border-radius:2px">';
1917
+ available.sort((a,b)=>a[1].name.localeCompare(b[1].name)).forEach(([id, p]) => {
1918
+ const onDesktop = desktopPages.includes(id);
1919
+ html += `<div style="padding:5px 8px;cursor:pointer;display:flex;align-items:center;gap:8px;border-bottom:1px solid #eee${onDesktop?' opacity:.5':''}" onclick="if(!${onDesktop})addShortcutToDesktop('${id}',this)">
1920
+ <span style="font-size:16px">${getIcon(getPageIconType(p.cat,id,p.name)).replace(/<[^>]+>/g,'') ? '📄' : '📄'}</span>
1921
+ <span style="flex:1">${p.name}</span>
1922
+ ${onDesktop ? '<span style="color:#999;font-size:10px">on desktop</span>' : ''}
1923
+ </div>`;
1924
+ });
1925
+ html += '</div></div>';
1926
+ createWindow({title:'Add Shortcut to Desktop', content:html, width:340, height:380});
1927
+ }
1928
+
1929
+ function addShortcutToDesktop(pageId, el) {
1930
+ if (!PAGES[pageId]) return;
1931
+ if (!desktopPages.includes(pageId)) desktopPages.push(pageId);
1932
+ if (!knownPages.includes(pageId)) knownPages.push(pageId);
1933
+ saveState();
1934
+ rebuildDesktopItems();
1935
+ buildDesktopIcons();
1936
+ // Mark as "on desktop" in the picker
1937
+ el.style.opacity = '0.5';
1938
+ const badge = document.createElement('span');
1939
+ badge.style.cssText = 'color:#999;font-size:10px';
1940
+ badge.textContent = 'on desktop';
1941
+ el.appendChild(badge);
1942
+ el.onclick = null;
1943
+ }
1944
+
1945
+ function renameIcon(item) {
1946
+ showPromptModal('Rename "' + getIconName(item) + '" to:', getIconName(item), (newName) => {
1947
+ if (!newName || !newName.trim() || newName.trim() === getIconName(item)) return;
1948
+ item.name = newName.trim();
1949
+ if (item.macName) item.macName = newName.trim();
1950
+ if (item.ubuntuName) item.ubuntuName = newName.trim();
1951
+ if (item.w31Name) item.w31Name = newName.trim();
1952
+ buildDesktopIcons();
1953
+ });
1954
+ }
1955
+
1956
+ function showProperties(item) {
1957
+ const isMac = currentTheme === 'mac';
1958
+ const isUbuntu = currentTheme === 'ubuntu';
1959
+ const title = (isMac||isUbuntu) ? getIconName(item) + ' Info' : getIconName(item) + ' Properties';
1960
+
1961
+ let type = 'System Folder';
1962
+ if (item.action === 'recycle') type = 'Recycle Bin';
1963
+ else if (item.action === 'explorer') type = 'System Folder';
1964
+ else if (item.action === 'folder' && item.category) {
1965
+ const cat = CATEGORIES[item.category];
1966
+ type = 'Folder (' + (cat ? Object.values(PAGES).filter(p=>p.cat===item.category).length : 0) + ' items)';
1967
+ }
1968
+ else if (item.action === 'customFolder' && item.folderId) {
1969
+ const cf = customFolders[item.folderId];
1970
+ type = 'Custom Folder (' + (cf ? cf.pages.length : 0) + ' items)';
1971
+ }
1972
+ else if (item.action === 'settings') type = 'Application';
1973
+ else if (item.action === 'internet') type = 'Application';
1974
+ else type = 'System Item';
1975
+
1976
+ const now = new Date().toLocaleDateString('en-US', {year:'numeric',month:'long',day:'numeric'});
1977
+ const content = `
1978
+ <div style="padding:16px;font-size:12px;display:flex;flex-direction:column;gap:12px">
1979
+ <div style="display:flex;align-items:center;gap:12px;padding-bottom:12px;border-bottom:1px solid #ccc">
1980
+ <div style="width:48px;height:48px">${getIcon(item.icon)}</div>
1981
+ <div>
1982
+ <div style="font-weight:bold;font-size:14px">${getIconName(item)}</div>
1983
+ <div style="color:#666;margin-top:2px">${type}</div>
1984
+ </div>
1985
+ </div>
1986
+ <table style="border-collapse:collapse;width:100%">
1987
+ <tr><td style="padding:3px 12px 3px 0;color:#666;white-space:nowrap">Type:</td><td style="padding:3px 0">${type}</td></tr>
1988
+ <tr><td style="padding:3px 12px 3px 0;color:#666;white-space:nowrap">Location:</td><td style="padding:3px 0">Desktop</td></tr>
1989
+ <tr><td style="padding:3px 12px 3px 0;color:#666;white-space:nowrap">Created:</td><td style="padding:3px 0">${now}</td></tr>
1990
+ <tr><td style="padding:3px 12px 3px 0;color:#666;white-space:nowrap">Modified:</td><td style="padding:3px 0">${now}</td></tr>
1991
+ ${item.category ? '<tr><td style="padding:3px 12px 3px 0;color:#666;white-space:nowrap">Contains:</td><td style="padding:3px 0">'+Object.values(PAGES).filter(p=>p.cat===item.category).length+' items</td></tr>' : ''}
1992
+ </table>
1993
+ </div>`;
1994
+
1995
+ createWindow({title, content, width:340, height:280});
1996
+ }
1997
+
1998
+ function showDesktopProperties() {
1999
+ const total = Object.keys(PAGES).length;
2000
+ const deleted = recycleBin.length;
2001
+ const content = `
2002
+ <div style="padding:16px;font-size:12px;display:flex;flex-direction:column;gap:12px">
2003
+ <div style="font-weight:bold;font-size:14px;padding-bottom:8px;border-bottom:1px solid #ccc">Desktop Information</div>
2004
+ <table style="border-collapse:collapse;width:100%">
2005
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Theme:</td><td style="padding:3px 0">${{xp:'Windows XP',mac:'macOS','95':'Windows 95','31':'Windows 3.1',ubuntu:'Ubuntu'}[currentTheme]}</td></tr>
2006
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Canvas Pages:</td><td style="padding:3px 0">${total} total</td></tr>
2007
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Desktop Icons:</td><td style="padding:3px 0">${DESKTOP_ITEMS.length}</td></tr>
2008
+ <tr><td style="padding:3px 12px 3px 0;color:#666">In Recycle Bin:</td><td style="padding:3px 0">${deleted}</td></tr>
2009
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Categories:</td><td style="padding:3px 0">${Object.keys(CATEGORIES).length}</td></tr>
2010
+ </table>
2011
+ </div>`;
2012
+ createWindow({title:'Desktop Properties', content, width:320, height:260});
2013
+ }
2014
+
2015
+ function closeAllMenus() {
2016
+ document.getElementById('context-menu').classList.remove('show');
2017
+ document.getElementById('start-menu').classList.remove('show');
2018
+ contextMenuOpen = false;
2019
+ startMenuOpen = false;
2020
+ }
2021
+
2022
+ function arrangeIcons() {
2023
+ iconPositions = {};
2024
+ saveState();
2025
+ buildDesktopIcons();
2026
+ }
2027
+
2028
+ /* ══════════════════════════════════════════════════════════════
2029
+ START MENU
2030
+ ══════════════════════════════════════════════════════════════ */
2031
+ function toggleStartMenu() {
2032
+ const menu = document.getElementById('start-menu');
2033
+ if (startMenuOpen) {
2034
+ closeAllMenus();
2035
+ return;
2036
+ }
2037
+ closeAllMenus();
2038
+ buildStartMenu();
2039
+ menu.classList.add('show');
2040
+ startMenuOpen = true;
2041
+ }
2042
+
2043
+ function buildStartMenu() {
2044
+ const menu = document.getElementById('start-menu');
2045
+ const isXP = currentTheme === 'xp';
2046
+ const is95 = currentTheme === '95';
2047
+ const isMac = currentTheme === 'mac';
2048
+ const isUbuntu = currentTheme === 'ubuntu';
2049
+ const is31 = currentTheme === '31';
2050
+
2051
+ if (isMac || is31 || isUbuntu) {
2052
+ // Dropdown menu style
2053
+ menu.style.cssText = '';
2054
+ if (isMac) {
2055
+ menu.style.cssText = 'top:25px;left:0;width:240px;background:rgba(40,40,40,0.85);backdrop-filter:blur(20px);border:0.5px solid rgba(255,255,255,0.15);border-radius:0 0 6px 6px;box-shadow:0 8px 30px rgba(0,0,0,0.35);padding:4px 0;color:rgba(255,255,255,0.9);';
2056
+ } else if (isUbuntu) {
2057
+ menu.style.cssText = 'top:25px;left:0;width:240px;background:rgba(48,48,48,0.95);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.1);border-radius:0 0 8px 8px;box-shadow:0 6px 24px rgba(0,0,0,0.4);padding:4px 0;color:rgba(255,255,255,0.9);font-family:Ubuntu,sans-serif;';
2058
+ } else {
2059
+ menu.style.cssText = 'top:22px;left:0;width:200px;background:#c0c0c0;border:2px solid #000;padding:2px 0;font-family:"Fixedsys","Courier New",monospace;';
2060
+ }
2061
+ let html = '';
2062
+ if (isMac) {
2063
+ html += '<div class="ctx-item" onclick="closeAllMenus()">About This Mac</div>';
2064
+ html += '<div class="ctx-separator"></div>';
2065
+ }
2066
+ if (isUbuntu) {
2067
+ html += '<div class="ctx-item" onclick="closeAllMenus()">About Ubuntu</div>';
2068
+ html += '<div class="ctx-separator"></div>';
2069
+ }
2070
+ html += '<div class="ctx-item" onclick="openThemeSettings();closeAllMenus()">'+(isMac?'System Preferences...':(isUbuntu?'Settings...':'Control Panel'))+'</div>';
2071
+ html += '<div class="ctx-separator"></div>';
2072
+ Object.keys(CATEGORIES).forEach(catId => {
2073
+ html += `<div class="ctx-item" onclick="openExplorer('${catId}');closeAllMenus()">${CATEGORIES[catId].name}</div>`;
2074
+ });
2075
+ html += '<div class="ctx-separator"></div>';
2076
+ html += '<div class="ctx-item" onclick="openExplorer();closeAllMenus()">'+(isMac?'Open Finder':(isUbuntu?'Files':'File Manager'))+'</div>';
2077
+ menu.innerHTML = html;
2078
+ return;
2079
+ }
2080
+
2081
+ // Windows XP / 95 start menu
2082
+ if (is95) {
2083
+ menu.style.cssText = '';
2084
+ } else {
2085
+ menu.style.cssText = '';
2086
+ }
2087
+
2088
+ let html = '';
2089
+ if (isXP) {
2090
+ html += `<div class="sm-header"><div class="sm-avatar" style="background:linear-gradient(135deg,#4a9eff,#1a5276)"></div><div class="sm-user">Mike</div></div>`;
2091
+ }
2092
+
2093
+ html += '<div class="sm-body">';
2094
+ html += '<div class="sm-left">';
2095
+ // Recent/pinned pages
2096
+ const pinned = ['bored-arcade','civ-sim','music-library','wolf3d-engine','office-hub'];
2097
+ pinned.forEach(pid => {
2098
+ const p = PAGES[pid];
2099
+ if (!p) return;
2100
+ html += `<div class="sm-item" onclick="openCanvasPage('${pid}');closeAllMenus()"><div class="sm-icon">${getIcon('document')}</div>${p.name}</div>`;
2101
+ });
2102
+ html += '<div class="sm-separator"></div>';
2103
+ // All Programs submenu - show categories
2104
+ Object.keys(CATEGORIES).forEach(catId => {
2105
+ html += `<div class="sm-item" onclick="openExplorer('${catId}');closeAllMenus()"><div class="sm-icon">${getIcon('folder')}</div>${CATEGORIES[catId].name}</div>`;
2106
+ });
2107
+ html += '</div>';
2108
+
2109
+ if (isXP) {
2110
+ html += '<div class="sm-right">';
2111
+ html += `<div class="sm-item" onclick="openExplorer();closeAllMenus()"><div class="sm-icon">${getIcon('computer')}</div>My Computer</div>`;
2112
+ html += `<div class="sm-item" onclick="openExplorer('ai-office');closeAllMenus()"><div class="sm-icon">${getIcon('folder')}</div>My Documents</div>`;
2113
+ html += `<div class="sm-item" onclick="openThemeSettings();closeAllMenus()"><div class="sm-icon">${getIcon('settings')}</div>Control Panel</div>`;
2114
+ html += '<div class="sm-separator"></div>';
2115
+ html += `<div class="sm-item" onclick="openRecycleBin();closeAllMenus()"><div class="sm-icon">${getIcon('recycle')}</div>Recycle Bin</div>`;
2116
+ html += '</div>';
2117
+ }
2118
+
2119
+ html += '</div>';
2120
+ html += '<div class="sm-footer">';
2121
+ html += `<button class="sm-footer-btn" onclick="closeAllMenus()">Log Off</button>`;
2122
+ html += `<button class="sm-footer-btn" onclick="closeAllMenus()">Shut Down</button>`;
2123
+ html += '</div>';
2124
+
2125
+ menu.innerHTML = html;
2126
+ }
2127
+
2128
+ /* ══════════════════════════════════════════════════════════════
2129
+ FILE EXPLORER
2130
+ ══════════════════════════════════════════════════════════════ */
2131
+ function openExplorer(activeCat) {
2132
+ activeCat = activeCat || Object.keys(CATEGORIES)[0];
2133
+ const explorerTitle = currentTheme === 'mac' ? 'Finder' : (currentTheme === 'ubuntu' ? 'Files' : (currentTheme === '31' ? 'File Manager' : 'My Computer'));
2134
+
2135
+ let sidebarHtml = '<div class="explorer-sidebar">';
2136
+ sidebarHtml += `<div class="cat-item${!activeCat ? ' active' : ''}" data-cat-id="__all__" onclick="switchExplorerCat(this, '__all__')">All Pages</div>`;
2137
+ Object.keys(CATEGORIES).forEach(catId => {
2138
+ const c = CATEGORIES[catId];
2139
+ const cnt = Object.values(PAGES).filter(p => p.cat === catId && !recycleBin.includes(catId)).length;
2140
+ sidebarHtml += `<div class="cat-item${catId===activeCat?' active':''}" data-cat-id="${catId}" onclick="switchExplorerCat(this, '${catId}')">${c.name} (${cnt})</div>`;
2141
+ });
2142
+ sidebarHtml += '</div>';
2143
+
2144
+ const gridHtml = buildExplorerGrid(activeCat);
2145
+
2146
+ const content = `
2147
+ <div class="explorer-toolbar">
2148
+ <button onclick="history.length">Back</button>
2149
+ <button onclick="">Forward</button>
2150
+ <button onclick="">Up</button>
2151
+ </div>
2152
+ <div class="explorer-addr"><span>Address:</span> ${CATEGORIES[activeCat]?.name || 'All'}</div>
2153
+ <div style="display:flex;flex:1;overflow:hidden">
2154
+ ${sidebarHtml}
2155
+ <div class="explorer-content" id="explorer-grid-area">${gridHtml}</div>
2156
+ </div>
2157
+ `;
2158
+
2159
+ createWindow({
2160
+ title: explorerTitle + ' - ' + (CATEGORIES[activeCat]?.name || 'All'),
2161
+ content, width: 700, height: 450,
2162
+ });
2163
+ }
2164
+
2165
+ function openCustomFolder(folderId) {
2166
+ const folder = customFolders[folderId];
2167
+ if (!folder) return;
2168
+ const explorerTitle = currentTheme === 'mac' ? 'Finder' : (currentTheme === 'ubuntu' ? 'Files' : 'Explorer');
2169
+ const gridHtml = buildCustomFolderGrid(folderId);
2170
+ const content = `
2171
+ <div class="explorer-toolbar">
2172
+ <button onclick="addPagesToFolder('${folderId}')">Add Pages\u2026</button>
2173
+ </div>
2174
+ <div class="explorer-addr"><span>Folder:</span> ${folder.name}</div>
2175
+ <div style="display:flex;flex:1;overflow:hidden">
2176
+ <div class="explorer-content" id="explorer-grid-area">${gridHtml}</div>
2177
+ </div>
2178
+ `;
2179
+ createWindow({
2180
+ title: explorerTitle + ' \u2013 ' + folder.name,
2181
+ content, width: 600, height: 400,
2182
+ });
2183
+ }
2184
+
2185
+ function buildCustomFolderGrid(folderId) {
2186
+ const folder = customFolders[folderId];
2187
+ if (!folder) return '';
2188
+ const pages = (folder.pages || []).filter(id => PAGES[id] && !recycleBin.includes(id));
2189
+ if (pages.length === 0) {
2190
+ return `<div style="padding:30px;text-align:center;color:#888">
2191
+ <div style="font-size:32px;margin-bottom:10px">📁</div>
2192
+ <div>Folder is empty</div>
2193
+ <div style="margin-top:8px"><button onclick="addPagesToFolder('${folderId}')" style="padding:4px 12px;cursor:pointer">Add Pages\u2026</button></div>
2194
+ </div>`;
2195
+ }
2196
+ let html = '<div class="explorer-grid">';
2197
+ pages.forEach(id => {
2198
+ const p = PAGES[id];
2199
+ if (!p) return;
2200
+ const eiIconHtml = p.iconUrl
2201
+ ? `<img src="${p.iconUrl}" style="width:100%;height:100%;object-fit:contain;">`
2202
+ : getIcon(getPageIconType(p.cat, id, p.name));
2203
+ html += `<div class="explorer-item" ondblclick="openCanvasPage('${id}')" oncontextmenu="event.preventDefault();event.stopPropagation();showFolderItemMenu(event,'${id}','${folderId}')">
2204
+ <div class="ei-icon">${eiIconHtml}</div>
2205
+ <div class="ei-name">${p.name}</div>
2206
+ </div>`;
2207
+ });
2208
+ html += '</div>';
2209
+ return html;
2210
+ }
2211
+
2212
+ function showFolderItemMenu(e, pageId, folderId) {
2213
+ const page = PAGES[pageId];
2214
+ const pageName = page ? page.name : pageId;
2215
+ const isMac = currentTheme === 'mac';
2216
+ showContextMenu(e.clientX, e.clientY, [
2217
+ {label:'Open', action:()=>openCanvasPage(pageId)},
2218
+ {label:'Open in New Tab', action:()=>openCanvasPage(pageId, true)},
2219
+ '---',
2220
+ {label:'Remove from Folder', action:()=>{
2221
+ if (customFolders[folderId]) {
2222
+ customFolders[folderId].pages = customFolders[folderId].pages.filter(id => id !== pageId);
2223
+ desktopPages.push(pageId); // put back on desktop
2224
+ saveState();
2225
+ rebuildDesktopItems();
2226
+ buildDesktopIcons();
2227
+ // Refresh the folder window
2228
+ document.querySelectorAll('.window').forEach(w => {
2229
+ const t = w.querySelector('.titlebar-title');
2230
+ if (t && t.textContent.includes(customFolders[folderId]?.name || '')) {
2231
+ const g = w.querySelector('#explorer-grid-area');
2232
+ if (g) g.innerHTML = buildCustomFolderGrid(folderId);
2233
+ }
2234
+ });
2235
+ }
2236
+ }},
2237
+ '---',
2238
+ {label:isMac?'Move to Trash':'Delete', action:()=>{
2239
+ showConfirmModal('Move "'+pageName+'" to Recycle Bin?', (ok)=>{
2240
+ if (!ok) return;
2241
+ if (customFolders[folderId]) customFolders[folderId].pages = customFolders[folderId].pages.filter(id => id !== pageId);
2242
+ deleteItem(pageId);
2243
+ saveState();
2244
+ rebuildDesktopItems();
2245
+ buildDesktopIcons();
2246
+ document.querySelectorAll('.window').forEach(w => {
2247
+ const t = w.querySelector('.titlebar-title');
2248
+ if (t && t.textContent.includes(customFolders[folderId]?.name || '')) {
2249
+ const g = w.querySelector('#explorer-grid-area');
2250
+ if (g) g.innerHTML = buildCustomFolderGrid(folderId);
2251
+ }
2252
+ });
2253
+ });
2254
+ }},
2255
+ '---',
2256
+ {label:'Properties', action:()=>showPageProperties(pageId)},
2257
+ ]);
2258
+ }
2259
+
2260
+ async function addPagesToFolder(folderId) {
2261
+ const folder = customFolders[folderId];
2262
+ if (!folder) return;
2263
+ // Force-sync manifest to ensure ALL pages are shown (including newly created ones)
2264
+ await fetchManifest(true);
2265
+ const available = Object.entries(PAGES).filter(([id]) =>
2266
+ !recycleBin.includes(id) && !folder.pages.includes(id)
2267
+ );
2268
+ if (!available.length) { showConfirmModal('All available pages are already in this folder.', ()=>{}); return; }
2269
+ let html = '<div style="padding:12px;font-size:12px;display:flex;flex-direction:column;height:100%">';
2270
+ html += `<div style="font-weight:bold;margin-bottom:8px">Add pages to "${folder.name}":</div>`;
2271
+ html += '<div style="flex:1;overflow-y:auto;border:1px solid #bbb;border-radius:2px;margin-bottom:10px">';
2272
+ available.sort((a,b)=>a[1].name.localeCompare(b[1].name)).forEach(([id, p]) => {
2273
+ html += `<label style="display:flex;align-items:center;gap:8px;padding:5px 8px;cursor:pointer;border-bottom:1px solid #eee">
2274
+ <input type="checkbox" value="${id}" style="flex-shrink:0">
2275
+ <span>${p.name}</span>
2276
+ </label>`;
2277
+ });
2278
+ html += '</div>';
2279
+ html += `<div style="display:flex;gap:6px;justify-content:flex-end">
2280
+ <button onclick="confirmAddToFolder('${folderId}',this)" style="padding:4px 14px;cursor:pointer;font-size:12px">Add Selected</button>
2281
+ <button onclick="closeWindow(parseInt(this.closest('.window').dataset.winId))" style="padding:4px 14px;cursor:pointer;font-size:12px">Cancel</button>
2282
+ </div></div>`;
2283
+ createWindow({title: 'Add Pages to "' + folder.name + '"', content: html, width: 360, height: 380});
2284
+ }
2285
+
2286
+ function confirmAddToFolder(folderId, btn) {
2287
+ const win = btn.closest('.window');
2288
+ const folder = customFolders[folderId];
2289
+ if (!folder) return;
2290
+ win.querySelectorAll('input[type=checkbox]:checked').forEach(cb => {
2291
+ const pid = cb.value;
2292
+ if (!folder.pages.includes(pid)) folder.pages.push(pid);
2293
+ desktopPages = desktopPages.filter(id => id !== pid);
2294
+ });
2295
+ saveState();
2296
+ rebuildDesktopItems();
2297
+ buildDesktopIcons();
2298
+ closeWindow(parseInt(win.dataset.winId));
2299
+ // Refresh any open folder windows and reopen
2300
+ openCustomFolder(folderId);
2301
+ }
2302
+
2303
+ function buildExplorerGrid(catId) {
2304
+ let pages;
2305
+ if (catId === '__all__') {
2306
+ pages = Object.entries(PAGES).filter(([id]) => !recycleBin.includes(id));
2307
+ } else {
2308
+ pages = Object.entries(PAGES).filter(([id, p]) => p.cat === catId && !recycleBin.includes(id));
2309
+ }
2310
+
2311
+ if (pages.length === 0) return '<div style="padding:20px;color:#888">No items</div>';
2312
+
2313
+ let html = '<div class="explorer-grid">';
2314
+ pages.forEach(([id, p]) => {
2315
+ const iconType = getPageIconType(p.cat, id, p.name);
2316
+ const catIconHtml = p.iconUrl
2317
+ ? `<img src="${p.iconUrl}" style="width:100%;height:100%;object-fit:contain;">`
2318
+ : getIcon(iconType);
2319
+ html += `<div class="explorer-item" ondblclick="openCanvasPage('${id}')" oncontextmenu="event.preventDefault();event.stopPropagation();showExplorerItemMenu(event,'${id}')">
2320
+ <div class="ei-icon">${catIconHtml}</div>
2321
+ <div class="ei-name">${p.name}</div>
2322
+ </div>`;
2323
+ });
2324
+ html += '</div>';
2325
+ return html;
2326
+ }
2327
+
2328
+ function getPageIconType(cat, pageId, pageName) {
2329
+ // Page-name overrides take priority — avoids all entertainment pages getting same icon
2330
+ const n = (pageName || pageId || '').toLowerCase();
2331
+ if (/music|song|audio|tts|voice.clone|suno/.test(n)) return 'music';
2332
+ if (/game|arcade|wolf|doom|quake|duke|play|shooter|civ.sim|bored/.test(n)) return 'game';
2333
+ if (/video|film|remotion/.test(n)) return 'folder';
2334
+ if (/book|doc|ref|guide|manual/.test(n)) return 'book';
2335
+ if (/tool|build|code|dev|api/.test(n)) return 'tools';
2336
+ const map = {
2337
+ 'entertainment':'game','tts-voice':'music','reference':'book',
2338
+ 'api-developer':'tools','business-tools':'tools',
2339
+ };
2340
+ return map[cat] || 'document';
2341
+ }
2342
+
2343
+ function switchExplorerCat(el, catId) {
2344
+ const sidebar = el.parentElement;
2345
+ sidebar.querySelectorAll('.cat-item').forEach(c => c.classList.remove('active'));
2346
+ el.classList.add('active');
2347
+ const grid = el.closest('.win-body').querySelector('#explorer-grid-area') ||
2348
+ el.closest('.win-body').querySelector('.explorer-content');
2349
+ if (grid) grid.innerHTML = buildExplorerGrid(catId);
2350
+ // Update address bar
2351
+ const addr = el.closest('.win-body').querySelector('.explorer-addr span');
2352
+ if (addr) addr.nextSibling.textContent = ' ' + (CATEGORIES[catId]?.name || 'All');
2353
+ }
2354
+
2355
+ function showExplorerItemMenu(e, pageId) {
2356
+ const isMac = currentTheme === 'mac';
2357
+ const isUbuntu = currentTheme === 'ubuntu';
2358
+ const page = PAGES[pageId];
2359
+ const pageName = page ? page.name : pageId;
2360
+
2361
+ const putOnDesktopAction = () => {
2362
+ delete savedPageCategories[pageId];
2363
+ if (!desktopPages.includes(pageId)) desktopPages.push(pageId);
2364
+ if (!knownPages.includes(pageId)) knownPages.push(pageId);
2365
+ saveState();
2366
+ refreshExplorers();
2367
+ };
2368
+ if (isMac || isUbuntu) {
2369
+ showContextMenu(e.clientX, e.clientY, [
2370
+ {label:'Open', action:()=>openCanvasPage(pageId)},
2371
+ {label:'Open in New Tab', action:()=>openCanvasPage(pageId, true)},
2372
+ '---',
2373
+ {label:'Put on Desktop', action:putOnDesktopAction},
2374
+ '---',
2375
+ {label:isMac?'Get Info':'Properties', action:()=>showPageProperties(pageId)},
2376
+ '---',
2377
+ {label:'Copy', action:()=>{clipboard={action:'copy',id:pageId}}},
2378
+ '---',
2379
+ {label:'Move to Trash', action:()=>{showConfirmModal('Move "'+pageName+'" to the Trash?',(ok)=>{if(ok){deleteItem(pageId);refreshExplorers();refreshRecycleBin();}});}},
2380
+ ]);
2381
+ } else {
2382
+ showContextMenu(e.clientX, e.clientY, [
2383
+ {label:'Open', action:()=>openCanvasPage(pageId)},
2384
+ {label:'Open in New Tab', action:()=>openCanvasPage(pageId, true)},
2385
+ '---',
2386
+ {label:'Put on Desktop', action:putOnDesktopAction},
2387
+ '---',
2388
+ {label:'Cut', action:()=>{clipboard={action:'cut',id:pageId,pageId:pageId}}},
2389
+ {label:'Copy', action:()=>{clipboard={action:'copy',id:pageId,pageId:pageId}}},
2390
+ '---',
2391
+ {label:'Delete', action:()=>{showConfirmModal('Move "'+pageName+'" to the Recycle Bin?',(ok)=>{if(ok){deleteItem(pageId);refreshExplorers();refreshRecycleBin();}});}},
2392
+ {label:'Rename', action:()=>{
2393
+ showPromptModal('Rename "'+pageName+'" to:', pageName, (n)=>{
2394
+ if(n && n.trim() && PAGES[pageId]) { PAGES[pageId].name = n.trim(); refreshExplorers(); }
2395
+ });
2396
+ }},
2397
+ '---',
2398
+ {label:'Properties', action:()=>showPageProperties(pageId)},
2399
+ ]);
2400
+ }
2401
+ }
2402
+
2403
+ function showPageProperties(pageId) {
2404
+ const page = PAGES[pageId];
2405
+ if (!page) return;
2406
+ const isMac = currentTheme === 'mac' || currentTheme === 'ubuntu';
2407
+ const title = (isMac ? page.name + ' Info' : page.name + ' Properties');
2408
+ const cat = CATEGORIES[page.cat];
2409
+ const now = new Date().toLocaleDateString('en-US', {year:'numeric',month:'long',day:'numeric'});
2410
+
2411
+ const content = `
2412
+ <div style="padding:16px;font-size:12px;display:flex;flex-direction:column;gap:12px">
2413
+ <div style="display:flex;align-items:center;gap:12px;padding-bottom:12px;border-bottom:1px solid #ccc">
2414
+ <div style="width:32px;height:32px">${getIcon(getPageIconType(page.cat))}</div>
2415
+ <div>
2416
+ <div style="font-weight:bold;font-size:14px">${page.name}</div>
2417
+ <div style="color:#666;margin-top:2px">Canvas Page</div>
2418
+ </div>
2419
+ </div>
2420
+ <table style="border-collapse:collapse;width:100%">
2421
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Type:</td><td style="padding:3px 0">HTML Canvas Page</td></tr>
2422
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Opens with:</td><td style="padding:3px 0">Canvas Viewer</td></tr>
2423
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Location:</td><td style="padding:3px 0">/pages/${pageId}.html</td></tr>
2424
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Category:</td><td style="padding:3px 0">${cat ? cat.name : 'Uncategorized'}</td></tr>
2425
+ <tr><td style="padding:3px 12px 3px 0;color:#666">Modified:</td><td style="padding:3px 0">${now}</td></tr>
2426
+ </table>
2427
+ <div style="padding-top:8px;border-top:1px solid #ccc">
2428
+ <button onclick="openCanvasPage('${pageId}')" style="padding:4px 16px;cursor:pointer;font-size:12px">Open</button>
2429
+ <button onclick="openCanvasPage('${pageId}',true)" style="padding:4px 16px;cursor:pointer;font-size:12px;margin-left:4px">Open in New Tab</button>
2430
+ </div>
2431
+ </div>`;
2432
+
2433
+ createWindow({title, content, width:380, height:300});
2434
+ }
2435
+
2436
+ function refreshExplorers() {
2437
+ rebuildDesktopItems();
2438
+ buildDesktopIcons();
2439
+ // Re-render any open explorer/folder windows
2440
+ document.querySelectorAll('.window').forEach(w => {
2441
+ const titleEl = w.querySelector('.titlebar-title');
2442
+ if (!titleEl) return;
2443
+ const gridArea = w.querySelector('#explorer-grid-area') || w.querySelector('.explorer-content');
2444
+ if (!gridArea) return;
2445
+ // Find active category from sidebar
2446
+ const activeCat = w.querySelector('.cat-item.active');
2447
+ if (activeCat) {
2448
+ const catId = activeCat.dataset.catId || '__all__';
2449
+ gridArea.innerHTML = buildExplorerGrid(catId);
2450
+ }
2451
+ });
2452
+ }
2453
+
2454
+ /* ══════════════════════════════════════════════════════════════
2455
+ OPEN CANVAS PAGE
2456
+ ══════════════════════════════════════════════════════════════ */
2457
+ function openCanvasPage(pageId, newWindow) {
2458
+ const page = PAGES[pageId];
2459
+ if (!page) return;
2460
+ if (newWindow) {
2461
+ window.open('/pages/' + pageId + '.html', '_blank');
2462
+ return;
2463
+ }
2464
+ // Use the parent app's postMessage bridge to navigate to the canvas page.
2465
+ // The desktop runs inside the OpenVoiceUI canvas iframe — sending a
2466
+ // 'navigate' action tells the parent to load the target page in this
2467
+ // same iframe, which is the standard canvas navigation mechanism.
2468
+ window.parent.postMessage({
2469
+ type: 'canvas-action',
2470
+ action: 'navigate',
2471
+ page: pageId,
2472
+ }, '*');
2473
+ }
2474
+
2475
+ /* ══════════════════════════════════════════════════════════════
2476
+ RECYCLE BIN
2477
+ ══════════════════════════════════════════════════════════════ */
2478
+ function openRecycleBin() {
2479
+ const title = currentTheme === 'mac' ? 'Trash' : 'Recycle Bin';
2480
+ let content = '<div class="recycle-toolbar">';
2481
+ content += '<button onclick="emptyRecycleBin();this.closest(\'.win-body\').querySelector(\'.explorer-content\').innerHTML=buildRecycleContent()">Empty</button>';
2482
+ content += '<button onclick="restoreAll();this.closest(\'.win-body\').querySelector(\'.explorer-content\').innerHTML=buildRecycleContent()">Restore All</button>';
2483
+ content += '</div>';
2484
+ content += '<div class="explorer-content">' + buildRecycleContent() + '</div>';
2485
+
2486
+ createWindow({title, content, width: 500, height: 350});
2487
+ }
2488
+
2489
+ function buildRecycleContent() {
2490
+ if (recycleBin.length === 0) return '<div class="recycle-empty">Recycle Bin is empty</div>';
2491
+ let html = '<div class="explorer-grid">';
2492
+ recycleBin.forEach(id => {
2493
+ const page = PAGES[id];
2494
+ const name = page ? page.name : CATEGORIES[id.replace('cat-','')]?.name || id;
2495
+ html += `<div class="explorer-item" ondblclick="restoreItem('${id}')" oncontextmenu="event.preventDefault();event.stopPropagation();showRecycleItemMenu(event,'${id}')">
2496
+ <div class="ei-icon">${getIcon('document')}</div>
2497
+ <div class="ei-name">${name}</div>
2498
+ </div>`;
2499
+ });
2500
+ html += '</div>';
2501
+ return html;
2502
+ }
2503
+
2504
+ // Central function — moves any desktop item to recycle bin
2505
+ function sendToRecycle(item) {
2506
+ if (!item || item.action === 'recycle') return;
2507
+ if (item.action === 'openPage' && item.pageId) {
2508
+ // Loose page icon
2509
+ desktopPages = desktopPages.filter(id => id !== item.pageId);
2510
+ deleteItem(item.pageId);
2511
+ } else {
2512
+ deleteItem(item.id);
2513
+ }
2514
+ delete iconPositions[item.id];
2515
+ rebuildDesktopItems();
2516
+ buildDesktopIcons();
2517
+ }
2518
+
2519
+ function showRecycleItemMenu(e, id) {
2520
+ const isMac = currentTheme === 'mac';
2521
+ const page = PAGES[id];
2522
+ const name = page ? page.name : CATEGORIES[id.replace('cat-','')]?.name || id;
2523
+ showContextMenu(e.clientX, e.clientY, [
2524
+ {label:'Restore', action:()=>restoreItem(id)},
2525
+ '---',
2526
+ {label:isMac?'Delete Immediately\u2026':'Delete Permanently\u2026', action:()=>{
2527
+ showConfirmModal('Permanently delete "'+name+'"? This cannot be undone.', (ok)=>{
2528
+ if (!ok) return;
2529
+ recycleBin = recycleBin.filter(i => i !== id);
2530
+ if (PAGES[id]) delete PAGES[id];
2531
+ knownPages = knownPages.filter(k => k !== id);
2532
+ desktopPages = desktopPages.filter(k => k !== id);
2533
+ saveState();
2534
+ rebuildDesktopItems();
2535
+ buildDesktopIcons();
2536
+ refreshRecycleBin();
2537
+ });
2538
+ }},
2539
+ ]);
2540
+ }
2541
+
2542
+ function deleteItem(id) {
2543
+ if (!recycleBin.includes(id)) {
2544
+ recycleBin.push(id);
2545
+ saveState();
2546
+ }
2547
+ }
2548
+
2549
+ function restoreItem(id) {
2550
+ recycleBin = recycleBin.filter(i => i !== id);
2551
+ // If it was a page, add back to desktop
2552
+ if (PAGES[id] && !desktopPages.includes(id)) desktopPages.push(id);
2553
+ saveState();
2554
+ rebuildDesktopItems();
2555
+ buildDesktopIcons();
2556
+ refreshRecycleBin();
2557
+ }
2558
+
2559
+ function restoreAll() {
2560
+ recycleBin.forEach(id => {
2561
+ if (PAGES[id] && !desktopPages.includes(id)) desktopPages.push(id);
2562
+ });
2563
+ recycleBin = [];
2564
+ saveState();
2565
+ rebuildDesktopItems();
2566
+ buildDesktopIcons();
2567
+ refreshRecycleBin();
2568
+ }
2569
+
2570
+ function emptyRecycleBin() {
2571
+ if (recycleBin.length === 0) return;
2572
+ // Remove permanently from known pages so they don't resurface
2573
+ recycleBin.forEach(id => {
2574
+ if (PAGES[id]) delete PAGES[id];
2575
+ knownPages = knownPages.filter(k => k !== id);
2576
+ });
2577
+ recycleBin = [];
2578
+ saveState();
2579
+ buildDesktopIcons();
2580
+ refreshRecycleBin();
2581
+ }
2582
+
2583
+ function refreshRecycleBin() {
2584
+ document.querySelectorAll('.window').forEach(w => {
2585
+ const title = w.querySelector('.titlebar-title');
2586
+ if (title && (title.textContent === 'Recycle Bin' || title.textContent === 'Trash')) {
2587
+ const content = w.querySelector('.explorer-content');
2588
+ if (content) content.innerHTML = buildRecycleContent();
2589
+ }
2590
+ });
2591
+ }
2592
+
2593
+ /* ══════════════════════════════════════════════════════════════
2594
+ THEME SETTINGS WINDOW
2595
+ ══════════════════════════════════════════════════════════════ */
2596
+ function openThemeSettings() {
2597
+ const themes = [
2598
+ {id:'xp',name:'Windows XP',preview:'linear-gradient(180deg,#1b58d0 0%,#3a82e6 40%,#5a9eec 60%,#4aad31 70%,#55bd3e 100%)'},
2599
+ {id:'mac',name:'macOS',preview:'linear-gradient(180deg,#0d1117 0%,#2c1654 30%,#1a5276 60%,#0f1a3e 100%)'},
2600
+ {id:'95',name:'Windows 95',preview:'linear-gradient(180deg,#008080 0%,#008080 70%,#c0c0c0 70%,#c0c0c0 100%)'},
2601
+ {id:'31',name:'Windows 3.1',preview:'linear-gradient(180deg,#00807f 0%,#00807f 80%,#c0c0c0 80%,#c0c0c0 100%)'},
2602
+ {id:'ubuntu',name:'Ubuntu',preview:'linear-gradient(180deg,#2c001e 0%,#77216f 30%,#5e2750 60%,#2c001e 80%,#303030 90%,#303030 100%)'},
2603
+ ];
2604
+
2605
+ let html = '<div class="theme-grid">';
2606
+ themes.forEach(t => {
2607
+ html += `<div class="theme-card ${t.id===currentTheme?'active':''}" onclick="setTheme('${t.id}');document.querySelectorAll('.theme-card').forEach(c=>c.classList.remove('active'));this.classList.add('active')">
2608
+ <div class="preview" style="background:${t.preview}"></div>
2609
+ <div class="label">${t.name}</div>
2610
+ </div>`;
2611
+ });
2612
+ html += '</div>';
2613
+
2614
+ // Wallpaper section
2615
+ const thumbHtml = wallpaperUrl
2616
+ ? `<img class="wallpaper-thumb" src="${wallpaperUrl}" onerror="this.style.display='none'">`
2617
+ : `<div class="wallpaper-empty">No<br>wallpaper</div>`;
2618
+ html += `<div class="wallpaper-section">
2619
+ <label>Desktop Wallpaper</label>
2620
+ <div class="wallpaper-row">
2621
+ ${thumbHtml}
2622
+ <div class="wallpaper-btns">
2623
+ <button class="wallpaper-btn" onclick="document.getElementById('wallpaper-file-input').click()">Choose Image&hellip;</button>
2624
+ ${wallpaperUrl ? `<button class="wallpaper-btn" onclick="removeWallpaper();this.closest('.win-body').querySelector('.wallpaper-section').innerHTML=buildWallpaperSection()">Remove Wallpaper</button>` : ''}
2625
+ </div>
2626
+ </div>
2627
+ </div>`;
2628
+
2629
+ createWindow({
2630
+ title: currentTheme === 'mac' ? 'System Preferences' : 'Display Properties',
2631
+ content: html, width: 520, height: 420,
2632
+ });
2633
+ }
2634
+
2635
+ /* ══════════════════════════════════════════════════════════════
2636
+ EVENT HANDLERS
2637
+ ══════════════════════════════════════════════════════════════ */
2638
+ document.getElementById('desktop').addEventListener('mousedown', (e) => {
2639
+ if (e.target === e.currentTarget || e.target.id === 'desktop') {
2640
+ selectedIcons.clear();
2641
+ updateIconSelection();
2642
+ closeAllMenus();
2643
+ }
2644
+ });
2645
+
2646
+ document.getElementById('desktop').addEventListener('contextmenu', (e) => {
2647
+ e.preventDefault();
2648
+ const iconEl = e.target.closest('.desktop-icon');
2649
+ if (iconEl) {
2650
+ const item = DESKTOP_ITEMS.find(i => i.id === iconEl.dataset.id);
2651
+ if (item) showContextMenu(e.clientX, e.clientY, getIconContextItems(item));
2652
+ } else if (e.target.closest('.window')) {
2653
+ // Don't show desktop context menu on windows
2654
+ return;
2655
+ } else {
2656
+ showContextMenu(e.clientX, e.clientY, getDesktopContextItems());
2657
+ }
2658
+ });
2659
+
2660
+ document.addEventListener('mousedown', (e) => {
2661
+ if (!e.target.closest('#context-menu') && contextMenuOpen) {
2662
+ document.getElementById('context-menu').classList.remove('show');
2663
+ contextMenuOpen = false;
2664
+ }
2665
+ if (!e.target.closest('#start-menu') && !e.target.closest('#start-btn') && !e.target.closest('.apple-logo') && startMenuOpen) {
2666
+ document.getElementById('start-menu').classList.remove('show');
2667
+ startMenuOpen = false;
2668
+ }
2669
+ });
2670
+
2671
+ // Resize handler — reflow icons and clamp windows on screen resize
2672
+ let _resizeTimer = null;
2673
+ window.addEventListener('resize', () => {
2674
+ clearTimeout(_resizeTimer);
2675
+ _resizeTimer = setTimeout(() => {
2676
+ buildDesktopIcons();
2677
+ const container = document.getElementById('os-container');
2678
+ const cw = container.offsetWidth;
2679
+ const dh = getDesktopHeight();
2680
+ const dTop = getDesktopTop();
2681
+ windows.forEach(w => {
2682
+ if (w.el.classList.contains('maximized')) {
2683
+ w.el.style.width = cw + 'px';
2684
+ w.el.style.height = dh + 'px';
2685
+ w.el.style.top = dTop + 'px';
2686
+ w.el.style.left = '0px';
2687
+ } else {
2688
+ // Clamp non-maximized windows to stay within desktop
2689
+ let ww = w.el.offsetWidth;
2690
+ let wh = w.el.offsetHeight;
2691
+ let wl = parseInt(w.el.style.left) || 0;
2692
+ let wt = parseInt(w.el.style.top) || 0;
2693
+ if (ww > cw) { w.el.style.width = cw + 'px'; ww = cw; }
2694
+ if (wh > dh) { w.el.style.height = dh + 'px'; wh = dh; }
2695
+ if (wl + ww > cw) w.el.style.left = Math.max(0, cw - ww) + 'px';
2696
+ if (wt + wh > dh + dTop) w.el.style.top = Math.max(dTop, dh + dTop - wh) + 'px';
2697
+ if (wt < dTop) w.el.style.top = dTop + 'px';
2698
+ }
2699
+ });
2700
+ }, 100);
2701
+ });
2702
+
2703
+ /* ══════════════════════════════════════════════════════════════
2704
+ MODAL DIALOG — replaces prompt/confirm (blocked in iframes)
2705
+ ══════════════════════════════════════════════════════════════ */
2706
+ let _modalCb = null;
2707
+ let _modalType = 'prompt';
2708
+
2709
+ function showPromptModal(message, defaultVal, callback) {
2710
+ _modalCb = callback;
2711
+ _modalType = 'prompt';
2712
+ document.getElementById('modal-message').textContent = message;
2713
+ const inp = document.getElementById('modal-input');
2714
+ inp.style.display = 'block';
2715
+ inp.value = defaultVal || '';
2716
+ document.getElementById('modal-cancel').style.display = '';
2717
+ document.getElementById('modal-ok').textContent = 'OK';
2718
+ document.getElementById('modal-overlay').classList.add('show');
2719
+ setTimeout(() => { inp.focus(); inp.select(); }, 50);
2720
+ }
2721
+
2722
+ function showConfirmModal(message, callback) {
2723
+ _modalCb = callback;
2724
+ _modalType = 'confirm';
2725
+ document.getElementById('modal-message').textContent = message;
2726
+ document.getElementById('modal-input').style.display = 'none';
2727
+ document.getElementById('modal-cancel').style.display = '';
2728
+ document.getElementById('modal-ok').textContent = 'OK';
2729
+ document.getElementById('modal-overlay').classList.add('show');
2730
+ }
2731
+
2732
+ function closeModal() {
2733
+ document.getElementById('modal-overlay').classList.remove('show');
2734
+ const cb = _modalCb; _modalCb = null;
2735
+ if (cb) cb(null);
2736
+ }
2737
+
2738
+ function submitModal() {
2739
+ document.getElementById('modal-overlay').classList.remove('show');
2740
+ const cb = _modalCb; _modalCb = null;
2741
+ if (!cb) return;
2742
+ if (_modalType === 'prompt') {
2743
+ cb(document.getElementById('modal-input').value);
2744
+ } else {
2745
+ cb(true);
2746
+ }
2747
+ }
2748
+
2749
+ document.addEventListener('keydown', (e) => {
2750
+ const overlay = document.getElementById('modal-overlay');
2751
+ if (overlay && overlay.classList.contains('show')) {
2752
+ if (e.key === 'Enter') { e.preventDefault(); submitModal(); }
2753
+ if (e.key === 'Escape') { e.preventDefault(); closeModal(); }
2754
+ return;
2755
+ }
2756
+ // Delete key — send selected desktop icons to recycle bin
2757
+ if (e.key === 'Delete' && selectedIcons.size > 0) {
2758
+ e.preventDefault();
2759
+ const toDelete = [...selectedIcons].map(id => DESKTOP_ITEMS.find(i => i.id === id)).filter(i => i && i.action !== 'recycle' && i.action !== 'explorer');
2760
+ if (!toDelete.length) return;
2761
+ const label = toDelete.length === 1 ? '"' + toDelete[0].name + '"' : toDelete.length + ' items';
2762
+ showConfirmModal('Move ' + label + ' to the Recycle Bin?', (ok) => {
2763
+ if (!ok) return;
2764
+ toDelete.forEach(item => sendToRecycle(item));
2765
+ selectedIcons.clear();
2766
+ updateIconSelection();
2767
+ });
2768
+ }
2769
+ if (e.key === 'Escape') closeAllMenus();
2770
+ });
2771
+
2772
+ /* ══════════════════════════════════════════════════════════════
2773
+ WALLPAPER
2774
+ ══════════════════════════════════════════════════════════════ */
2775
+ function applyWallpaper(url) {
2776
+ wallpaperUrl = url || null;
2777
+ const desktop = document.getElementById('desktop');
2778
+ if (url) {
2779
+ // Include a dark fallback color so nothing shows through while the image loads
2780
+ desktop.style.background = 'url(' + url + ') center/cover no-repeat #1a1a1a';
2781
+ } else {
2782
+ desktop.style.background = '';
2783
+ }
2784
+ }
2785
+
2786
+ async function uploadWallpaper(file) {
2787
+ if (!file) return;
2788
+ const fd = new FormData();
2789
+ fd.append('file', file);
2790
+ try {
2791
+ const opts = { method: 'POST', body: fd };
2792
+ if(window._canvasAuthToken) opts.headers = { 'Authorization': 'Bearer ' + window._canvasAuthToken };
2793
+ const resp = await fetch('/api/upload', opts);
2794
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
2795
+ const data = await resp.json();
2796
+ const url = data.url || data.path;
2797
+ if (!url) throw new Error('No URL in response');
2798
+ applyWallpaper(url);
2799
+ saveState();
2800
+ // Refresh wallpaper section in any open Display Properties window
2801
+ document.querySelectorAll('.win-body .wallpaper-section').forEach(el => {
2802
+ el.outerHTML = buildWallpaperSection();
2803
+ });
2804
+ } catch (err) {
2805
+ showConfirmModal('Wallpaper upload failed: ' + err.message, () => {});
2806
+ }
2807
+ }
2808
+
2809
+ function removeWallpaper() {
2810
+ applyWallpaper(null);
2811
+ saveState();
2812
+ }
2813
+
2814
+ function handleWallpaperFile(input) {
2815
+ if (input.files && input.files[0]) {
2816
+ uploadWallpaper(input.files[0]);
2817
+ input.value = ''; // reset so same file can be re-selected
2818
+ }
2819
+ }
2820
+
2821
+ function buildWallpaperSection() {
2822
+ const thumbHtml = wallpaperUrl
2823
+ ? `<img class="wallpaper-thumb" src="${wallpaperUrl}" onerror="this.style.display='none'">`
2824
+ : `<div class="wallpaper-empty">No<br>wallpaper</div>`;
2825
+ return `<div class="wallpaper-section">
2826
+ <label>Desktop Wallpaper</label>
2827
+ <div class="wallpaper-row">
2828
+ ${thumbHtml}
2829
+ <div class="wallpaper-btns">
2830
+ <button class="wallpaper-btn" onclick="document.getElementById('wallpaper-file-input').click()">Choose Image&hellip;</button>
2831
+ ${wallpaperUrl ? `<button class="wallpaper-btn" onclick="removeWallpaper();this.closest('.wallpaper-section').outerHTML=buildWallpaperSection()">Remove Wallpaper</button>` : ''}
2832
+ </div>
2833
+ </div>
2834
+ </div>`;
2835
+ }
2836
+
2837
+ /* ══════════════════════════════════════════════════════════════
2838
+ INIT
2839
+ ══════════════════════════════════════════════════════════════ */
2840
+ async function init() {
2841
+ await loadState();
2842
+ await fetchManifest();
2843
+ // Ensure DOM is fully laid out before measuring
2844
+ requestAnimationFrame(() => {
2845
+ setTheme(currentTheme);
2846
+ updateClock();
2847
+ });
2848
+ // Poll for new pages every 30 seconds, pause when tab hidden
2849
+ _manifestTimer = setInterval(fetchManifest, 30000);
2850
+ document.addEventListener('visibilitychange', () => {
2851
+ if (document.hidden) {
2852
+ if (_manifestTimer) { clearInterval(_manifestTimer); _manifestTimer = null; }
2853
+ } else {
2854
+ if (!_manifestTimer) { fetchManifest(); _manifestTimer = setInterval(fetchManifest, 30000); }
2855
+ }
2856
+ });
2857
+ }
2858
+ if (document.readyState === 'loading') {
2859
+ document.addEventListener('DOMContentLoaded', init);
2860
+ } else {
2861
+ init();
2862
+ }
2863
+ </script>
2864
+ </body>
2865
+ </html>