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.
- package/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- 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()"></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…</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…</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>
|