Repository navigation
Expand file tree
/
Copy pathmain.js
More file actions
574 lines (525 loc) · 22.8 KB
/
Copy pathmain.js
File metadata and controls
574 lines (525 loc) · 22.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
// InstantWeb LivePreviewer v1.3.0 — by TechSetuApps
const PLUGIN_ID = 'com.techsetuapps.instantweb.livepreviewer';
const AMAZON_LINK = 'https://github.com/TechSetuApps/InstantLiveServer/releases/tag/v1.4.0';
// Port — user saved port ya default 7090
function getSavedPort() {
try { return localStorage.getItem('iw_port') || '7090'; } catch(e) { return '7090'; }
}
function savePort(p) {
try { localStorage.setItem('iw_port', p); } catch(e) {}
}
// Custom path — /data.json, /style.css etc
function getSavedPath() {
try { return localStorage.getItem('iw_path') || '/'; } catch(e) { return '/'; }
}
function savePath(p) {
try { localStorage.setItem('iw_path', p || '/'); } catch(e) {}
}
function getPreviewUrl() {
return 'http://127.0.0.1:' + getSavedPort();
}
function getFullUrl() {
const path = getSavedPath();
return getPreviewUrl() + (path.startsWith('/') ? path : '/' + path);
}
// Address bar display:
// Normal mode → shows file/page title (or path if not root)
// Edit mode → shows "localhost:PORT/path" — fully editable
let panel=null, iframe=null, fab=null;
// ACode ka root container dhundo
function getAcodeRoot() {
return (
document.querySelector('#root') ||
document.querySelector('.app') ||
document.querySelector('#app') ||
document.body
);
}
// #quick-tools ka top position dhundho
// Panel ka bottom = quicktools ka top
function getQuickToolsOffset() {
const qt = document.querySelector('#quick-tools');
if (!qt) return 0;
const rect = qt.getBoundingClientRect();
if (rect.height === 0 || rect.top <= 0) return 0;
return window.innerHeight - rect.top;
}
// Panel ko body mein add karo
function appendToAcode(el) {
document.body.appendChild(el);
}
// Panel bottom = quicktools top pe exactly fit karo
function adjustPanelBottom() {
if (!panel || isFs || resizing) return; // resize ke dauran override mat karo!
const qt = document.querySelector('#quick-tools');
if (!qt) return;
const rect = qt.getBoundingClientRect();
if (rect.height === 0 || rect.top <= 0) return;
const offset = window.innerHeight - rect.top;
panel.style.bottom = offset + 'px';
panel.style.position = 'fixed';
}
let isVisible=false, isFs=false, resizing=false, resizeTimer;
let addrEditMode = false; // false = title mode, true = edit mode
function getViewportTop() {
return window.visualViewport ? window.visualViewport.offsetTop : 0;
}
function getViewportHeight() {
return window.visualViewport ? window.visualViewport.height : window.innerHeight;
}
function applyViewportPosition() {
if (!panel || isFs) return;
const addr = document.getElementById('iw-addr');
if (addr && document.activeElement === addr) return;
const isLand = window.innerWidth > window.innerHeight;
if (isLand) {
panel.style.top = getViewportTop() + 'px';
panel.style.height = getViewportHeight() + 'px';
}
// Portrait me keyboard ke liye kuch nahi karo
// adjustPanelBottom sirf panel open pe ek baar call hoga
}
function initViewportListener() {
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', applyViewportPosition);
window.visualViewport.addEventListener('scroll', applyViewportPosition);
}
}
function destroyViewportListener() {
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', applyViewportPosition);
window.visualViewport.removeEventListener('scroll', applyViewportPosition);
}
}
const ic = {
refresh: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>`,
close: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
fullscr: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`,
exitfs: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="10" y1="14" x2="3" y2="21"/><line x1="21" y1="3" x2="14" y2="10"/></svg>`,
dl: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
};
function mkBtn(html, title, color, cb) {
const b = document.createElement('button');
b.innerHTML = html; b.title = title;
b.style.cssText = `display:flex;align-items:center;justify-content:center;width:34px;height:34px;background:transparent;border:none;cursor:pointer;border-radius:8px;color:${color};flex-shrink:0;-webkit-tap-highlight-color:transparent;`;
let _t = false;
b.addEventListener('touchstart', () => b.style.background='rgba(255,255,255,0.12)', {passive:true});
b.addEventListener('touchend', e => { b.style.background='transparent'; _t=true; e.preventDefault(); cb(); }, {passive:false});
b.addEventListener('click', () => { if(_t){_t=false;return;} cb(); });
return b;
}
// Update address bar display
// Normal mode: show page title or path
// Edit mode: show "localhost:PORT/path"
function updateAddrDisplay(titleText) {
const addr = document.getElementById('iw-addr');
if (!addr) return;
if (addrEditMode) return; // editing — don't override
const path = getSavedPath();
if (titleText) {
addr.value = titleText;
addr.title = 'localhost:' + getSavedPort() + path + '';
} else if (path && path !== '/') {
addr.value = path; // show path if non-root
addr.title = 'localhost:' + getSavedPort() + path;
} else {
addr.value = 'Live Preview';
addr.title = 'localhost:' + getSavedPort();
}
}
function enterAddrEdit() {
const addr = document.getElementById('iw-addr');
if (!addr) return;
addrEditMode = true;
const path = getSavedPath();
addr.readOnly = false;
addr.style.cursor = 'text';
addr.style.textAlign = 'left';
// locLabel already shows "localhost:" — so show only PORT/path
addr.value = getSavedPort() + path;
addr.select();
}
function exitAddrEdit(save) {
const addr = document.getElementById('iw-addr');
if (!addr) return;
addrEditMode = false;
addr.readOnly = true;
addr.style.cursor = 'pointer';
addr.style.textAlign = 'center';
if (save) {
let val = addr.value.trim();
// Remove "localhost:" prefix if user accidentally typed it
val = val.replace(/^localhost:/i, '');
// Split port and path
const slashIdx = val.indexOf('/');
let port, path;
if (slashIdx === -1) {
port = val;
path = '/';
} else {
port = val.substring(0, slashIdx);
path = val.substring(slashIdx) || '/';
}
// Validate port
if (port && !isNaN(port) && +port >= 1 && +port <= 65535) {
savePort(port);
}
// Save path
savePath(path);
loadPreview();
}
updateAddrDisplay(null);
}
function showError() {
if (!panel) return;
const old = document.getElementById('iw-err'); if (old) old.remove();
const d = document.createElement('div');
d.id = 'iw-err';
d.style.cssText = `position:absolute;left:0;right:0;bottom:0;top:0;background:#18181b;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:28px;text-align:center;z-index:5;`;
d.innerHTML = `
<div style="width:68px;height:68px;background:linear-gradient(135deg,#2563eb,#1d4ed8);border-radius:18px;display:flex;align-items:center;justify-content:center;font-size:34px;font-weight:900;color:#fff;margin-bottom:20px;box-shadow:0 8px 28px rgba(255,255,255,0.08);">W</div>
<p style="color:#e2e8f0;font-size:15px;font-weight:700;margin-bottom:8px;">InstantLive Server is not running</p>
<p style="color:#64748b;font-size:12px;margin-bottom:22px;line-height:1.7;max-width:230px;">Open InstantLive Server, select an HTML file and tap Start Hosting.</p>
<a href="${AMAZON_LINK}" style="display:flex;align-items:center;gap:8px;background:#2563eb;color:#fff;padding:11px 22px;border-radius:11px;text-decoration:none;font-size:13px;font-weight:700;margin-bottom:12px;">${ic.dl} Download InstantLive Server</a>
<button id="iw-retry" style="background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.1);color:#94a3b8;padding:9px 20px;border-radius:9px;cursor:pointer;font-size:12px;">Retry</button>
${getSavedPort() !== '7090' ? '<button id="iw-reset-port" style="background:transparent;border:none;color:#475569;font-size:11px;margin-top:8px;cursor:pointer;text-decoration:underline;">Reset to default (localhost:7090)</button>' : ''}
`;
const webArea = panel.querySelector('#iw-webarea') || panel;
webArea.appendChild(d);
document.getElementById('iw-retry').onclick = () => { d.remove(); loadPreview(); };
const resetBtn = document.getElementById('iw-reset-port');
if (resetBtn) resetBtn.onclick = () => {
savePort('7090');
savePath('/');
d.remove();
loadPreview();
};
}
// Read <title> from ACode editor session — no cross-origin issues
// ACode WebView is file:// origin, InstantWeb is http:// — contentDocument is always blocked
// So we read the title directly from the open file's editor content instead
function getTitleFromEditor() {
try {
const file = editorManager && editorManager.activeFile;
if (!file) return null;
const session = file.session;
if (session) {
const content = session.getValue();
const m = content.match(/<title[^>]*>([^<\n]+)<\/title>/i);
if (m && m[1].trim()) return m[1].trim();
}
return file.name || null;
} catch(e) {
return null;
}
}
function loadPreview() {
if (!iframe) return;
const sb = document.getElementById('iw-sb');
if (sb) sb.style.background = '#f59e0b';
iframe.removeAttribute('srcdoc');
const baseUrl = getPreviewUrl();
const path = getSavedPath();
fetch(baseUrl + '/__iw_ping?t=' + Date.now(), {mode:'no-cors', cache:'no-store'})
.then(() => {
const isInstantWeb = getSavedPort() === '7090';
let finalUrl;
if (path && path !== '/') {
finalUrl = baseUrl + path + (path.includes('?') ? '&' : '?') + 't=' + Date.now();
} else {
finalUrl = baseUrl + (isInstantWeb ? '/?__iw_acode=1&t=' : '/?t=') + Date.now();
}
// onload BEFORE src — never misses the load event
iframe.onload = () => {
if (sb) sb.style.background = '#22c55e';
if (path && path !== '/') {
updateAddrDisplay(path);
} else {
// Read title from ACode editor content (reliable — no cross-origin block)
const t = getTitleFromEditor();
updateAddrDisplay(t || 'Live Preview');
}
};
iframe.src = finalUrl;
})
.catch(() => {
showError();
if (sb) sb.style.background = '#ef4444';
});
}
function toggleFullscreen() {
if (!panel) return;
isFs = !isFs;
if (isFs) {
destroyViewportListener();
panel.style.cssText = `position:fixed;left:0;top:0;width:100%;height:100%;background:#18181b;z-index:9999;display:flex;flex-direction:column;`;
} else {
// Fullscreen exit — poora panel rebuild karo fresh orientation ke saath
// Taaki resize handle landscape → portrait switch pe sahi rahe
if (panel) { panel.remove(); panel = null; iframe = null; }
isVisible = false;
buildPanel();
}
const fsBtn = document.getElementById('iw-fs-btn');
if (fsBtn) fsBtn.innerHTML = isFs ? ic.exitfs : ic.fullscr;
}
function addResizeHandle(p, isLand) {
// Outer invisible touch target — bada touch area
const h = document.createElement('div');
h.style.cssText = `position:absolute;background:transparent;z-index:51;${isLand?'left:0;top:50%;transform:translateY(-50%);width:24px;height:80px;cursor:ew-resize;':'top:0;left:50%;transform:translateX(-50%);height:24px;width:80px;cursor:ns-resize;'}`;
// Inner visible pill
const pill = document.createElement('div');
pill.style.cssText = `position:absolute;background:rgba(255,255,255,0.2);border-radius:4px;${isLand?'right:0;top:50%;transform:translateY(-50%);width:4px;height:36px;':'top:0;left:50%;transform:translateX(-50%);height:4px;width:44px;'}`;
h.appendChild(pill);
h.onmouseenter = () => { pill.style.background='rgba(99,153,255,0.6)'; pill.style.transform=isLand?'translateY(-50%) scaleX(1.5)':'translateX(-50%) scaleY(1.5)'; };
h.onmouseleave = () => { if(!resizing){ pill.style.background='rgba(255,255,255,0.2)'; pill.style.transform=isLand?'translateY(-50%)':'translateX(-50%)'; } };
let s0, sz0;
const start = (x,y) => {
resizing=true; s0=isLand?x:y; sz0=isLand?p.offsetWidth:p.offsetHeight;
// bottom reset mat karo — quicktools ke upar hi rahega
};
const move = (x,y) => {
if(!resizing) return;
if(isLand) {
const v = Math.max(180, Math.min(window.innerWidth*0.82, sz0+(s0-x)));
p.style.width = v+'px';
} else {
const v = Math.max(120, Math.min(window.innerHeight*0.88, sz0+(s0-y)));
p.style.height = v+'px';
}
};
h.addEventListener('touchstart', e=>{start(e.touches[0].clientX,e.touches[0].clientY);e.preventDefault();},{passive:false});
document.addEventListener('touchmove', e=>{if(resizing){move(e.touches[0].clientX,e.touches[0].clientY);e.preventDefault();}},{passive:false});
document.addEventListener('touchend', ()=>{ resizing=false; });
h.onmousedown = e => start(e.clientX, e.clientY);
document.addEventListener('mousemove', e => move(e.clientX, e.clientY));
document.addEventListener('mouseup', ()=>{ resizing=false; });
p.appendChild(h);
}
function buildPanel() {
if (panel) { panel.remove(); panel=null; iframe=null; }
destroyViewportListener();
addrEditMode = false;
// Dimensions update karo — stale state se resize bug hota tha
_lastW = window.innerWidth;
_lastH = window.innerHeight;
const isLand = window.innerWidth > window.innerHeight;
const vTop = getViewportTop();
const vH = getViewportHeight();
const panelH = Math.round(window.innerHeight * 0.30);
panel = document.createElement('div');
panel.id = 'iw-panel';
panel.style.cssText = `
position:fixed;background:#18181b;z-index:50;
display:flex;flex-direction:column;
${isLand
? `right:0;top:${vTop}px;width:46%;height:${vH}px;border-left:1px solid rgba(255,255,255,0.08);box-shadow:-4px 0 32px rgba(0,0,0,0.6);`
: `left:0;bottom:0;width:100%;height:${panelH}px;border-top:1px solid rgba(255,255,255,0.08);border-radius:20px 20px 0 0;box-shadow:0 -8px 40px rgba(0,0,0,0.5);transition:bottom 0.15s;`
}
`;
const hd = document.createElement('div');
hd.style.cssText = `display:flex;align-items:center;padding:0 10px;background:rgba(30,30,35,0.98);gap:8px;flex-shrink:0;height:54px;border-bottom:1px solid rgba(255,255,255,0.06);${!isLand?'border-radius:16px 16px 0 0;':''}`;
// W Logo — favicon milne pe replace ho jaayega
const ico = document.createElement('div');
ico.id = 'iw-logo';
ico.style.cssText = `width:40px;height:40px;background:linear-gradient(135deg,#2563eb,#1d4ed8);border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:900;color:#fff;flex-shrink:0;box-shadow:0 2px 8px rgba(255,255,255,0.1);overflow:hidden;`;
ico.textContent = 'W';
// Address bar wrap
const addrWrap = document.createElement('div');
addrWrap.style.cssText = `flex:1;display:flex;align-items:center;background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:0 10px;height:34px;min-width:0;`;
// "localhost:" fixed prefix label
const locLabel = document.createElement('span');
locLabel.textContent = 'localhost:';
locLabel.style.cssText = `color:#60A5FA;font-size:12px;font-weight:600;flex-shrink:0;font-family:monospace;opacity:0.85;display:none;`;
locLabel.id = 'iw-loc-label';
// Address input — shows title normally, full url when editing
const addrText = document.createElement('input');
addrText.id = 'iw-addr';
addrText.type = 'text';
addrText.value = 'Live Preview';
addrText.readOnly = true;
addrText.style.cssText = `flex:1;background:none;border:none;color:#e5e5ea;font-size:13px;font-weight:500;outline:none;text-align:center;font-family:-apple-system,sans-serif;cursor:pointer;min-width:0;`;
// Tap → enter edit mode
addrText.addEventListener('click', () => {
if (!addrEditMode) {
enterAddrEdit();
// Show localhost: prefix
locLabel.style.display = 'block';
addrText.style.textAlign = 'left';
}
});
addrText.addEventListener('touchend', e => {
if (!addrEditMode) {
e.preventDefault();
enterAddrEdit();
locLabel.style.display = 'block';
addrText.style.textAlign = 'left';
setTimeout(() => addrText.focus(), 50);
}
}, {passive:false});
addrText.addEventListener('blur', () => {
exitAddrEdit(true);
locLabel.style.display = 'none';
addrText.style.textAlign = 'center';
});
addrText.addEventListener('keydown', e => {
if (e.key === 'Enter') addrText.blur();
if (e.key === 'Escape') {
addrEditMode = false;
exitAddrEdit(false);
locLabel.style.display = 'none';
addrText.style.textAlign = 'center';
}
});
// Status dot
const sb = document.createElement('span');
sb.id = 'iw-sb';
sb.style.cssText = `width:8px;height:8px;border-radius:50%;background:#475569;flex-shrink:0;margin-left:4px;`;
addrWrap.appendChild(locLabel);
addrWrap.appendChild(addrText);
addrWrap.appendChild(sb);
const refreshB = mkBtn(ic.refresh, 'Refresh', '#94a3b8', () => {
const e = document.getElementById('iw-err');
if (e) e.remove();
loadPreview();
});
const closeB = mkBtn(ic.close, 'Close', '#ef4444', hidePanel);
const fsBtn = mkBtn(ic.fullscr, 'Fullscreen', '#94a3b8', toggleFullscreen);
fsBtn.id = 'iw-fs-btn';
hd.appendChild(ico);
hd.appendChild(addrWrap);
hd.appendChild(refreshB);
hd.appendChild(fsBtn);
hd.appendChild(closeB);
const webArea = document.createElement('div');
webArea.id = 'iw-webarea';
webArea.style.cssText = `flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative;min-height:0;`;
iframe = document.createElement('iframe');
iframe.style.cssText = `flex:1;border:none;background:#fff;width:100%;height:100%;display:block;`;
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-modals');
webArea.appendChild(iframe);
panel.appendChild(hd);
panel.appendChild(webArea);
addResizeHandle(panel, isLand);
appendToAcode(panel);
isVisible = true;
initViewportListener();
updateFab();
// DOM settle hone ke baad position fix karo — rAF x2
requestAnimationFrame(() => requestAnimationFrame(() => adjustPanelBottom()));
loadPreview();
}
function hidePanel() {
if (panel) { panel.remove(); panel=null; iframe=null; }
isVisible=false; isFs=false; addrEditMode=false;
destroyViewportListener();
updateFab();
}
function togglePanel() {
isVisible && panel ? hidePanel() : buildPanel();
}
function createFab() {
fab = document.createElement('div');
fab.id = 'iw-fab';
fab.style.cssText = `
position:fixed;top:64px;right:14px;
width:40px;height:40px;
background:linear-gradient(135deg,#2563eb,#1d4ed8);
border-radius:12px;
display:flex;align-items:center;justify-content:center;
font-size:18px;font-weight:900;color:#fff;
z-index:51;
box-shadow:0 4px 20px rgba(255,255,255,0.08);
user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;
`;
fab.textContent = 'W';
fab.title = 'InstantLive Server LivePreviewer (Ctrl+Shift+W)';
let dragging=false, moved=false, offX=0, offY=0, startT=0;
fab.addEventListener('touchstart', e => {
const t=e.touches[0], r=fab.getBoundingClientRect();
offX=t.clientX-r.left; offY=t.clientY-r.top;
dragging=true; moved=false; startT=Date.now();
e.preventDefault();
}, {passive:false});
document.addEventListener('touchmove', e => {
if(!dragging) return;
const t=e.touches[0];
const dx=Math.abs(t.clientX-offX-fab.getBoundingClientRect().left);
const dy=Math.abs(t.clientY-offY-fab.getBoundingClientRect().top);
if(dx>5||dy>5) moved=true;
fab.style.left = Math.max(0, Math.min(window.innerWidth-48, t.clientX-offX))+'px';
fab.style.top = Math.max(0, Math.min(window.innerHeight-48, t.clientY-offY))+'px';
fab.style.right='auto'; fab.style.bottom='auto';
e.preventDefault();
}, {passive:false});
document.addEventListener('touchend', () => {
if(!dragging) return;
dragging=false;
if(!moved || Date.now()-startT<200) togglePanel();
});
appendToAcode(fab);
}
function updateFab() {
if(!fab) return;
fab.style.background = isVisible
? 'linear-gradient(135deg,#1d4ed8,#1e1e1e)'
: 'linear-gradient(135deg,#2563eb,#1d4ed8)';
}
let _lastW=window.innerWidth, _lastH=window.innerHeight;
function onResize() {
const w=window.innerWidth, h=window.innerHeight;
const addr=document.getElementById('iw-addr');
if(addr && document.activeElement===addr) return;
if(w!==_lastW||h!==_lastH){
_lastW=w; _lastH=h;
clearTimeout(resizeTimer);
resizeTimer=setTimeout(()=>{ if(isVisible&&panel&&!isFs) buildPanel(); }, 320);
}
}
acode.setPluginInit(PLUGIN_ID, () => {
try {
const commands = acode.require('commands');
if (commands && commands.addCommand) {
commands.addCommand({
name:'iw-preview',
description:'Toggle InstantLive Server Preview',
bindKey:{win:'Ctrl-Shift-W',mac:'Ctrl-Shift-W'},
exec:togglePanel,
});
} else {
editorManager.editor.commands.addCommand({
name:'iw-preview',
bindKey:{win:'Ctrl-Shift-W',mac:'Ctrl-Shift-W'},
exec:togglePanel,
});
}
} catch(e) {
try {
editorManager.editor.commands.addCommand({
name:'iw-preview',
bindKey:{win:'Ctrl-Shift-W',mac:'Ctrl-Shift-W'},
exec:togglePanel,
});
} catch(e2) {}
}
createFab();
window.addEventListener('orientationchange', onResize);
window.addEventListener('resize', onResize);
window.toast && window.toast('InstantLive Server LivePreviewer ready!', 2500);
});
acode.setPluginUnmount(PLUGIN_ID, () => {
hidePanel();
if(fab) fab.remove();
window.removeEventListener('orientationchange', onResize);
window.removeEventListener('resize', onResize);
try {
const commands = acode.require('commands');
if (commands && commands.removeCommand) {
commands.removeCommand('iw-preview');
} else {
editorManager.editor.commands.removeCommand('iw-preview');
}
} catch(e) {
try { editorManager.editor.commands.removeCommand('iw-preview'); } catch(e2) {}
}
});