@macle-community/simulator-web 0.0.0-stage → 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.
@@ -0,0 +1,510 @@
1
+ // Macle MiniProgram Web Simulator Controller
2
+ (function() {
3
+ 'use strict';
4
+
5
+ const DEVICES = [
6
+ { name: 'Huawei Mate 60 Pro', width: 392, height: 848, dpr: 3, notch: false },
7
+ { name: 'Huawei Pura 70 Ultra', width: 400, height: 900, dpr: 3, notch: true },
8
+ { name: 'iPhone 15 Pro', width: 393, height: 852, dpr: 3, notch: true },
9
+ { name: 'iPhone 14 Pro Max', width: 430, height: 932, dpr: 3, notch: true },
10
+ { name: 'iPhone 13', width: 390, height: 844, dpr: 3, notch: true },
11
+ { name: 'iPhone 8 / SE', width: 375, height: 667, dpr: 2, notch: false },
12
+ { name: 'Nexus 6', width: 412, height: 732, dpr: 3.5, notch: false },
13
+ { name: 'iPad Pro 11"', width: 834, height: 1194, dpr: 2, notch: false }
14
+ ];
15
+
16
+ let currentDevice = DEVICES[0];
17
+ let currentScale = 1.0;
18
+ let isLandscape = false;
19
+ let touchEnabled = false;
20
+ let autoReloadEnabled = true;
21
+ let projectInfo = null;
22
+ const logs = [];
23
+ const networkRequests = [];
24
+
25
+ // DOM Elements
26
+ const deviceSelect = document.getElementById('device-select');
27
+ const pageSelect = document.getElementById('page-select');
28
+ const scaleSelect = document.getElementById('scale-select');
29
+ const rotateBtn = document.getElementById('rotate-btn');
30
+ const refreshBtn = document.getElementById('refresh-btn');
31
+ const touchBtn = document.getElementById('touch-btn');
32
+ const networkSelect = document.getElementById('network-select');
33
+ const qrBtn = document.getElementById('qr-btn');
34
+ const devtoolsBtn = document.getElementById('devtools-btn');
35
+ const devtoolsDrawer = document.getElementById('devtools-drawer');
36
+ const closeDevtoolsBtn = document.getElementById('close-devtools');
37
+ const clearLogsBtn = document.getElementById('clear-logs');
38
+ const filterInput = document.getElementById('filter-logs');
39
+ const logsContainer = document.getElementById('logs-container');
40
+ const evalInput = document.getElementById('eval-input');
41
+ const evalForm = document.getElementById('eval-form');
42
+ const deviceWrapper = document.getElementById('device-wrapper');
43
+ const deviceFrame = document.getElementById('device-frame');
44
+ const deviceNotch = document.getElementById('device-notch');
45
+ const simulatorIframe = document.getElementById('simulator-iframe');
46
+ const touchCursor = document.getElementById('touch-cursor');
47
+ const pagePathEl = document.getElementById('page-path');
48
+ const copyPathBtn = document.getElementById('copy-path-btn');
49
+ const qrModal = document.getElementById('qr-modal');
50
+ const closeQrModalBtn = document.getElementById('close-qr-modal');
51
+ const qrcodeContainer = document.getElementById('qrcode-container');
52
+ const qrUrlEl = document.getElementById('qr-url');
53
+ const copyQrUrlBtn = document.getElementById('copy-qr-url-btn');
54
+
55
+ // DevTools Tabs
56
+ const devtoolsTabs = document.querySelectorAll('.devtools-tab');
57
+ const tabPanes = {
58
+ 'console': document.getElementById('tab-console'),
59
+ 'storage': document.getElementById('tab-storage'),
60
+ 'network': document.getElementById('tab-network'),
61
+ 'app-info': document.getElementById('tab-app-info')
62
+ };
63
+ const storageBody = document.getElementById('storage-body');
64
+ const refreshStorageBtn = document.getElementById('refresh-storage-btn');
65
+ const networkBody = document.getElementById('network-body');
66
+ const clearNetworkBtn = document.getElementById('clear-network-btn');
67
+
68
+ // App Info Elements
69
+ const infoProjectName = document.getElementById('info-project-name');
70
+ const infoEngineVersion = document.getElementById('info-engine-version');
71
+ const infoRuntimeVersion = document.getElementById('info-runtime-version');
72
+ const infoEntryPage = document.getElementById('info-entry-page');
73
+ const infoPagesList = document.getElementById('info-pages-list');
74
+
75
+ // Populate Device Select
76
+ DEVICES.forEach((d, idx) => {
77
+ const opt = document.createElement('option');
78
+ opt.value = idx;
79
+ opt.textContent = `${d.name} (${d.width}×${d.height})`;
80
+ deviceSelect.appendChild(opt);
81
+ });
82
+
83
+ // Apply device size
84
+ function updateDeviceDimensions() {
85
+ let w = currentDevice.width;
86
+ let h = currentDevice.height;
87
+ if (isLandscape) {
88
+ const tmp = w; w = h; h = tmp;
89
+ }
90
+ deviceFrame.style.width = `${w + 24}px`;
91
+ deviceFrame.style.height = `${h + 24}px`;
92
+
93
+ if (currentDevice.notch && !isLandscape) {
94
+ deviceNotch.style.display = 'flex';
95
+ } else {
96
+ deviceNotch.style.display = 'none';
97
+ }
98
+ applyScale();
99
+ }
100
+
101
+ // Apply zoom scale
102
+ function applyScale() {
103
+ if (scaleSelect.value === 'fit') {
104
+ const canvas = document.querySelector('.canvas');
105
+ const pad = 60;
106
+ const availW = canvas.clientWidth - pad;
107
+ const availH = canvas.clientHeight - pad;
108
+ let w = isLandscape ? currentDevice.height : currentDevice.width;
109
+ let h = isLandscape ? currentDevice.width : currentDevice.height;
110
+ const scaleW = availW / (w + 24);
111
+ const scaleH = availH / (h + 24);
112
+ currentScale = Math.min(1.0, scaleW, scaleH);
113
+ } else {
114
+ currentScale = parseFloat(scaleSelect.value);
115
+ }
116
+ deviceWrapper.style.transform = `scale(${currentScale})`;
117
+ }
118
+
119
+ // Event Listeners for Controls
120
+ deviceSelect.addEventListener('change', (e) => {
121
+ currentDevice = DEVICES[parseInt(e.target.value, 10)];
122
+ updateDeviceDimensions();
123
+ sendToSimulator({
124
+ command: 'SYSTEM_INFO_CHANGE',
125
+ inputParams: {
126
+ screenWidth: currentDevice.width,
127
+ screenHeight: currentDevice.height,
128
+ pixelRatio: currentDevice.dpr
129
+ }
130
+ });
131
+ });
132
+
133
+ scaleSelect.addEventListener('change', applyScale);
134
+ window.addEventListener('resize', () => {
135
+ if (scaleSelect.value === 'fit') applyScale();
136
+ });
137
+
138
+ rotateBtn.addEventListener('click', () => {
139
+ isLandscape = !isLandscape;
140
+ rotateBtn.classList.toggle('active', isLandscape);
141
+ updateDeviceDimensions();
142
+ });
143
+
144
+ refreshBtn.addEventListener('click', () => {
145
+ refreshBtn.classList.add('active');
146
+ simulatorIframe.src = simulatorIframe.src;
147
+ setTimeout(() => refreshBtn.classList.remove('active'), 500);
148
+ });
149
+
150
+ // Touch Simulation
151
+ touchBtn.addEventListener('click', () => {
152
+ touchEnabled = !touchEnabled;
153
+ touchBtn.classList.toggle('active', touchEnabled);
154
+ touchCursor.style.display = touchEnabled ? 'block' : 'none';
155
+ });
156
+
157
+ window.addEventListener('mousemove', (e) => {
158
+ if (!touchEnabled) return;
159
+ touchCursor.style.left = `${e.clientX}px`;
160
+ touchCursor.style.top = `${e.clientY}px`;
161
+ });
162
+
163
+ window.addEventListener('mousedown', (e) => {
164
+ if (!touchEnabled) return;
165
+ const ripple = document.createElement('div');
166
+ ripple.className = 'touch-ripple';
167
+ ripple.style.left = `${e.clientX}px`;
168
+ ripple.style.top = `${e.clientY}px`;
169
+ document.body.appendChild(ripple);
170
+ setTimeout(() => ripple.remove(), 400);
171
+ });
172
+
173
+ // Network Selection
174
+ networkSelect.addEventListener('change', (e) => {
175
+ const net = e.target.value;
176
+ sendToSimulator({
177
+ callbackId: 1,
178
+ command: 'NETWORK_STATUS_CHANGE',
179
+ inputParams: { type: net }
180
+ });
181
+ addLog('info', `Network status changed to: ${net}`);
182
+ });
183
+
184
+ // DevTools Toggle
185
+ devtoolsBtn.addEventListener('click', () => {
186
+ devtoolsDrawer.classList.toggle('open');
187
+ devtoolsBtn.classList.toggle('active', devtoolsDrawer.classList.contains('open'));
188
+ });
189
+
190
+ closeDevtoolsBtn.addEventListener('click', () => {
191
+ devtoolsDrawer.classList.remove('open');
192
+ devtoolsBtn.classList.remove('active');
193
+ });
194
+
195
+ clearLogsBtn.addEventListener('click', () => {
196
+ logs.length = 0;
197
+ renderLogs();
198
+ });
199
+
200
+ filterInput.addEventListener('input', renderLogs);
201
+
202
+ // Tab Switching
203
+ devtoolsTabs.forEach(tab => {
204
+ tab.addEventListener('click', () => {
205
+ devtoolsTabs.forEach(t => t.classList.remove('active'));
206
+ tab.classList.add('active');
207
+ const targetKey = tab.dataset.tab;
208
+ Object.keys(tabPanes).forEach(k => {
209
+ if (tabPanes[k]) {
210
+ tabPanes[k].style.display = k === targetKey ? (k === 'console' ? 'flex' : 'block') : 'none';
211
+ }
212
+ });
213
+ if (targetKey === 'storage') renderStorage();
214
+ });
215
+ });
216
+
217
+ // Storage Inspector
218
+ function renderStorage() {
219
+ if (!storageBody) return;
220
+ try {
221
+ const iframeWin = simulatorIframe.contentWindow;
222
+ const keys = [];
223
+ if (iframeWin && iframeWin.localStorage) {
224
+ for (let i = 0; i < iframeWin.localStorage.length; i++) {
225
+ const k = iframeWin.localStorage.key(i);
226
+ keys.push({ key: k, value: iframeWin.localStorage.getItem(k) });
227
+ }
228
+ }
229
+ if (keys.length === 0) {
230
+ storageBody.innerHTML = '<tr><td colspan="3" style="padding:10px;text-align:center;color:var(--text-muted);">No storage items found</td></tr>';
231
+ return;
232
+ }
233
+ storageBody.innerHTML = '';
234
+ keys.forEach(item => {
235
+ const tr = document.createElement('tr');
236
+ tr.style.borderBottom = '1px solid var(--border-color)';
237
+ tr.innerHTML = `
238
+ <td style="padding:6px;color:var(--accent-color);">${escapeHtml(item.key)}</td>
239
+ <td style="padding:6px;color:var(--text-primary);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(item.value)}</td>
240
+ <td style="padding:6px;"><button class="tool-btn" style="padding:2px 6px;font-size:10px;" onclick="window.__removeStorage('${escapeHtml(item.key)}')">Delete</button></td>
241
+ `;
242
+ storageBody.appendChild(tr);
243
+ });
244
+ } catch (e) {
245
+ storageBody.innerHTML = `<tr><td colspan="3" style="padding:10px;color:var(--danger-color);">${e.message}</td></tr>`;
246
+ }
247
+ }
248
+
249
+ window.__removeStorage = function(key) {
250
+ try {
251
+ const iframeWin = simulatorIframe.contentWindow;
252
+ if (iframeWin && iframeWin.localStorage) {
253
+ iframeWin.localStorage.removeItem(key);
254
+ renderStorage();
255
+ }
256
+ } catch (e) {}
257
+ };
258
+
259
+ if (refreshStorageBtn) refreshStorageBtn.addEventListener('click', renderStorage);
260
+
261
+ // Network Inspector
262
+ function addNetworkRequest(method, url, status, time) {
263
+ networkRequests.push({ method, url, status, time });
264
+ renderNetwork();
265
+ }
266
+
267
+ function renderNetwork() {
268
+ if (!networkBody) return;
269
+ if (networkRequests.length === 0) {
270
+ networkBody.innerHTML = '<tr><td colspan="4" style="padding:10px;text-align:center;color:var(--text-muted);">No network requests recorded</td></tr>';
271
+ return;
272
+ }
273
+ networkBody.innerHTML = '';
274
+ networkRequests.forEach(req => {
275
+ const tr = document.createElement('tr');
276
+ tr.style.borderBottom = '1px solid var(--border-color)';
277
+ const statusColor = (req.status >= 200 && req.status < 300) ? 'var(--success-color)' : 'var(--danger-color)';
278
+ tr.innerHTML = `
279
+ <td style="padding:6px;color:var(--accent-color);font-weight:bold;">${req.method}</td>
280
+ <td style="padding:6px;color:${statusColor};">${req.status}</td>
281
+ <td style="padding:6px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(req.url)}</td>
282
+ <td style="padding:6px;color:var(--text-muted);">${req.time}</td>
283
+ `;
284
+ networkBody.appendChild(tr);
285
+ });
286
+ }
287
+
288
+ if (clearNetworkBtn) {
289
+ clearNetworkBtn.addEventListener('click', () => {
290
+ networkRequests.length = 0;
291
+ renderNetwork();
292
+ });
293
+ }
294
+
295
+ // DevTools JS Evaluator
296
+ evalForm.addEventListener('submit', (e) => {
297
+ e.preventDefault();
298
+ const code = evalInput.value.trim();
299
+ if (!code) return;
300
+ addLog('info', `> ${code}`);
301
+ evalInput.value = '';
302
+ try {
303
+ const res = simulatorIframe.contentWindow.eval(code);
304
+ addLog('log', `< ${typeof res === 'object' ? JSON.stringify(res) : String(res)}`);
305
+ } catch (err) {
306
+ addLog('error', `< ${err.message}`);
307
+ }
308
+ });
309
+
310
+ // Logs management
311
+ function addLog(level, msg) {
312
+ const time = new Date().toLocaleTimeString();
313
+ logs.push({ level, msg, time });
314
+ if (logs.length > 500) logs.shift();
315
+ renderLogs();
316
+ }
317
+
318
+ function renderLogs() {
319
+ const filter = filterInput.value.toLowerCase();
320
+ logsContainer.innerHTML = '';
321
+ const filtered = logs.filter(l => !filter || l.msg.toLowerCase().includes(filter));
322
+ filtered.forEach(l => {
323
+ const entry = document.createElement('div');
324
+ entry.className = 'log-entry';
325
+ entry.innerHTML = `
326
+ <span class="log-time">${l.time}</span>
327
+ <span class="log-level ${l.level}">${l.level}</span>
328
+ <span class="log-msg">${escapeHtml(l.msg)}</span>
329
+ `;
330
+ logsContainer.appendChild(entry);
331
+ });
332
+ logsContainer.scrollTop = logsContainer.scrollHeight;
333
+ }
334
+
335
+ function escapeHtml(str) {
336
+ return String(str)
337
+ .replace(/&/g, '&amp;')
338
+ .replace(/</g, '&lt;')
339
+ .replace(/>/g, '&gt;')
340
+ .replace(/"/g, '&quot;');
341
+ }
342
+
343
+ // Communication with simulator iframe
344
+ function sendToSimulator(msg) {
345
+ try {
346
+ simulatorIframe.contentWindow.postMessage(msg, '*');
347
+ } catch (e) {
348
+ console.warn('Failed to send message to simulator:', e);
349
+ }
350
+ }
351
+
352
+ window.addEventListener('message', (event) => {
353
+ const data = event.data;
354
+ if (!data) return;
355
+
356
+ // Handle cur-page-res
357
+ if (data.channel === 'cur-page-res' && data.args && data.args[0]) {
358
+ const p = data.args[0];
359
+ pagePathEl.textContent = p;
360
+ pagePathEl.title = p;
361
+ if (pageSelect) {
362
+ pageSelect.value = p;
363
+ }
364
+ }
365
+
366
+ // Handle miniprogram console logs
367
+ if (data.type === 'macle-console') {
368
+ const msg = data.args.join(' ');
369
+ addLog(data.level, msg);
370
+ }
371
+
372
+ // Handle network requests logging
373
+ if (data.type === 'macle-network') {
374
+ addNetworkRequest(data.method || 'GET', data.url, data.status || 200, new Date().toLocaleTimeString());
375
+ }
376
+ });
377
+
378
+ // Copy Path
379
+ copyPathBtn.addEventListener('click', () => {
380
+ const text = pagePathEl.textContent;
381
+ if (text && text !== '-') {
382
+ navigator.clipboard.writeText(text).then(() => {
383
+ copyPathBtn.title = 'Copied!';
384
+ setTimeout(() => copyPathBtn.title = 'Copy Path', 1500);
385
+ });
386
+ }
387
+ });
388
+
389
+ // QR Code Modal
390
+ qrBtn.addEventListener('click', () => {
391
+ qrModal.classList.add('open');
392
+ if (!qrcodeContainer.hasChildNodes() && window.QRCode) {
393
+ new QRCode(qrcodeContainer, {
394
+ text: qrUrlEl.textContent,
395
+ width: 180,
396
+ height: 180,
397
+ colorDark: '#0f172a',
398
+ colorLight: '#ffffff',
399
+ correctLevel: QRCode.CorrectLevel.M
400
+ });
401
+ }
402
+ });
403
+
404
+ closeQrModalBtn.addEventListener('click', () => {
405
+ qrModal.classList.remove('open');
406
+ });
407
+
408
+ copyQrUrlBtn.addEventListener('click', () => {
409
+ navigator.clipboard.writeText(qrUrlEl.textContent).then(() => {
410
+ copyQrUrlBtn.textContent = 'Copied!';
411
+ setTimeout(() => copyQrUrlBtn.textContent = 'Copy URL', 1500);
412
+ });
413
+ });
414
+
415
+ // Setup Live Reload EventSource
416
+ function setupLiveReload() {
417
+ if (!window.EventSource) return;
418
+ try {
419
+ const es = new EventSource('/api/live-reload');
420
+ es.onmessage = (e) => {
421
+ if (e.data === 'reload' && autoReloadEnabled) {
422
+ addLog('info', 'File change detected, auto-reloading simulator...');
423
+ simulatorIframe.src = simulatorIframe.src;
424
+ }
425
+ };
426
+ es.onerror = () => {
427
+ setTimeout(setupLiveReload, 5000);
428
+ };
429
+ } catch (e) {
430
+ console.warn('Live reload unavailable:', e);
431
+ }
432
+ }
433
+
434
+ // Page Selection
435
+ if (pageSelect) {
436
+ pageSelect.addEventListener('change', (e) => {
437
+ const page = e.target.value;
438
+ if (!page) return;
439
+ try {
440
+ if (simulatorIframe.contentWindow && typeof simulatorIframe.contentWindow.goToPage === 'function') {
441
+ simulatorIframe.contentWindow.goToPage(page);
442
+ addLog('info', `Navigated simulator to: ${page}`);
443
+ return;
444
+ }
445
+ } catch (err) {}
446
+ const proj = projectInfo ? projectInfo.projectName : 'dist';
447
+ simulatorIframe.src = `/${proj}/root/debug/simulator/index.html?entryPagePath=${encodeURIComponent(page)}`;
448
+ addLog('info', `Switched entry page to: ${page}`);
449
+ });
450
+ }
451
+
452
+ function renderAppInfo(info) {
453
+ if (infoProjectName) infoProjectName.textContent = info.projectName || 'Macle App';
454
+ if (infoEngineVersion) infoEngineVersion.textContent = info.engineVersion || '26.6.0-LG0001';
455
+ if (infoRuntimeVersion) infoRuntimeVersion.textContent = info.runtimeVersion || '100.4.2';
456
+ if (infoEntryPage) infoEntryPage.textContent = info.entryPagePath || 'pages/index/index';
457
+ if (infoPagesList && Array.isArray(info.pages)) {
458
+ infoPagesList.innerHTML = '';
459
+ info.pages.forEach(p => {
460
+ const li = document.createElement('li');
461
+ li.textContent = p;
462
+ infoPagesList.appendChild(li);
463
+ });
464
+ }
465
+ }
466
+
467
+ // Initialize
468
+ function init() {
469
+ updateDeviceDimensions();
470
+ setupLiveReload();
471
+
472
+ const urlParams = new URLSearchParams(window.location.search);
473
+ const targetPage = urlParams.get('page') || urlParams.get('entryPagePath');
474
+ const entryQuery = targetPage ? `?entryPagePath=${encodeURIComponent(targetPage)}` : '';
475
+
476
+ // Fetch project info
477
+ fetch('/api/project-info')
478
+ .then(res => res.json())
479
+ .then(info => {
480
+ projectInfo = info;
481
+ renderAppInfo(info);
482
+
483
+ if (info.pages && Array.isArray(info.pages) && pageSelect) {
484
+ pageSelect.innerHTML = '';
485
+ info.pages.forEach(p => {
486
+ const opt = document.createElement('option');
487
+ opt.value = p;
488
+ opt.textContent = p;
489
+ pageSelect.appendChild(opt);
490
+ });
491
+ if (targetPage) pageSelect.value = targetPage;
492
+ }
493
+
494
+ const finalUrl = info.simulatorUrl + entryQuery;
495
+ simulatorIframe.src = finalUrl;
496
+ const fullUrl = new URL(finalUrl, window.location.href).href;
497
+ qrUrlEl.textContent = fullUrl;
498
+ addLog('info', `Connected to Macle project: ${info.projectName}`);
499
+ addLog('info', `Dual-thread runtime initialized: ${finalUrl}`);
500
+ })
501
+ .catch(err => {
502
+ const simUrl = `/dist/root/debug/simulator/index.html${entryQuery}`;
503
+ simulatorIframe.src = simUrl;
504
+ qrUrlEl.textContent = new URL(simUrl, window.location.href).href;
505
+ addLog('info', `Loaded fallback simulator at: ${simUrl}`);
506
+ });
507
+ }
508
+
509
+ window.addEventListener('DOMContentLoaded', init);
510
+ })();