| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>CodeMaster Remote Viewer</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/lucide@latest"></script> |
| <style> |
| body { margin: 0; background: #0f172a; color: #e2e8f0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; } |
| #preview-frame { width: 100vw; height: 100vh; border: 0; background: white; } |
| #console-panel { position: fixed; bottom: 0; left: 0; right: 0; height: 200px; background: rgba(15, 23, 42, 0.95); border-top: 2px solid #334155; display: none; } |
| #console-panel.visible { display: block; } |
| .console-line { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; padding: 2px 8px; border-left: 3px solid transparent; } |
| .console-line.error { color: #ef4444; border-left-color: #ef4444; } |
| .console-line.warn { color: #f59e0b; border-left-color: #f59e0b; } |
| .console-line.info { color: #60a5fa; border-left-color: #60a5fa; } |
| .console-line.log { color: #e2e8f0; } |
| #status-bar { position: fixed; top: 0; left: 0; right: 0; height: 32px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; font-size: 12px; z-index: 100; } |
| #sync-status { display: flex; align-items: center; gap: 6px; } |
| .status-dot { width: 8px; height: 8px; border-radius: 50%; } |
| .status-connected { background: #22c55e; } |
| .status-disconnected { background: #ef4444; } |
| .status-syncing { background: #3b82f6; animation: pulse 1s infinite; } |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } |
| </style> |
| </head> |
| <body> |
| <div id="status-bar"> |
| <div class="flex items-center gap-4"> |
| <div class="flex items-center gap-2 text-blue-400 font-semibold"> |
| <i data-lucide="smartphone" class="w-4 h-4"></i> |
| <span>Remote Viewer</span> |
| </div> |
| <div id="sync-status"> |
| <div id="status-dot" class="status-dot status-disconnected"></div> |
| <span id="status-text">Connecting...</span> |
| </div> |
| </div> |
| <div class="flex items-center gap-3"> |
| <button onclick="toggleConsole()" class="hover:text-blue-400 transition flex items-center gap-1"> |
| <i data-lucide="terminal" class="w-4 h-4"></i> Console |
| </button> |
| <button onclick="reloadPreview()" class="hover:text-blue-400 transition flex items-center gap-1"> |
| <i data-lucide="refresh-cw" class="w-4 h-4"></i> Reload |
| </button> |
| </div> |
| </div> |
|
|
| <div style="margin-top: 32px; height: calc(100vh - 32px);"> |
| <iframe id="preview-frame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"></iframe> |
| </div> |
|
|
| <div id="console-panel"> |
| <div class="flex items-center justify-between px-3 py-2 border-b border-slate-700 bg-slate-800"> |
| <span class="font-semibold text-sm">Console Output</span> |
| <button onclick="clearConsole()" class="text-xs hover:text-red-400 transition">Clear</button> |
| </div> |
| <div id="console-output" class="h-full overflow-y-auto pb-8"></div> |
| </div> |
|
|
| <script> |
| lucide.createIcons(); |
| |
| let bc = null; |
| let reconnectInterval = null; |
| let currentContent = ''; |
| let isConsoleVisible = false; |
| |
| |
| function initSync() { |
| if ('BroadcastChannel' in window) { |
| try { |
| bc = new BroadcastChannel('codemaster_sync'); |
| setupBroadcastChannel(); |
| } catch (e) { |
| console.log('BroadcastChannel not available, using polling'); |
| startPolling(); |
| } |
| } else { |
| startPolling(); |
| } |
| } |
| |
| function setupBroadcastChannel() { |
| bc.onmessage = (event) => { |
| if (event.data.type === 'code_update') { |
| updatePreview(event.data.content); |
| updateStatus('connected', 'Connected'); |
| } else if (event.data.type === 'ping') { |
| bc.postMessage({ type: 'pong', timestamp: Date.now() }); |
| } |
| }; |
| |
| |
| bc.postMessage({ type: 'viewer_ready', timestamp: Date.now() }); |
| updateStatus('connected', 'Connected via BroadcastChannel'); |
| } |
| |
| function startPolling() { |
| updateStatus('syncing', 'Syncing...'); |
| |
| setInterval(() => { |
| const data = localStorage.getItem('codemaster_preview'); |
| if (data && data !== currentContent) { |
| currentContent = data; |
| try { |
| const parsed = JSON.parse(data); |
| updatePreview(parsed.content); |
| updateStatus('connected', 'Connected via Storage'); |
| } catch (e) { |
| console.error('Parse error:', e); |
| } |
| } |
| }, 500); |
| |
| |
| window.addEventListener('storage', (e) => { |
| if (e.key === 'codemaster_preview') { |
| try { |
| const parsed = JSON.parse(e.newValue); |
| updatePreview(parsed.content); |
| updateStatus('connected', 'Connected'); |
| } catch (err) {} |
| } |
| }); |
| } |
| |
| function updatePreview(content) { |
| const frame = document.getElementById('preview-frame'); |
| |
| |
| const wrappedContent = ` |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <script> |
| (function() { |
| const originalLog = console.log; |
| const originalError = console.error; |
| const originalWarn = console.warn; |
| const originalInfo = console.info; |
| |
| function sendToParent(type, args) { |
| try { |
| const message = { |
| type: 'console', |
| level: type, |
| args: Array.from(args).map(arg => { |
| try { |
| return typeof arg === 'object' ? JSON.stringify(arg) : String(arg); |
| } catch(e) { |
| return String(arg); |
| } |
| }), |
| timestamp: Date.now() |
| }; |
| |
| if (window.parent !== window) { |
| window.parent.postMessage(message, '*'); |
| } |
| |
| // Also try BroadcastChannel |
| if (typeof BroadcastChannel !== 'undefined') { |
| const bc = new BroadcastChannel('codemaster_sync'); |
| bc.postMessage(message); |
| bc.close(); |
| } |
| } catch(e) {} |
| } |
| |
| console.log = function(...args) { |
| sendToParent('log', args); |
| originalLog.apply(console, args); |
| }; |
| console.error = function(...args) { |
| sendToParent('error', args); |
| originalError.apply(console, args); |
| }; |
| console.warn = function(...args) { |
| sendToParent('warn', args); |
| originalWarn.apply(console, args); |
| }; |
| console.info = function(...args) { |
| sendToParent('info', args); |
| originalInfo.apply(console, args); |
| }; |
| |
| window.onerror = function(msg, url, line) { |
| sendToParent('error', [msg + ' (line ' + line + ')']); |
| }; |
| })(); |
| <\/script> |
| </head> |
| <body> |
| ${content} |
| </body> |
| </html> |
| `; |
| |
| const blob = new Blob([wrappedContent], { type: 'text/html' }); |
| const url = URL.createObjectURL(blob); |
| |
| |
| try { |
| const scrollPos = frame.contentWindow?.scrollY || 0; |
| frame.src = url; |
| frame.onload = () => { |
| try { |
| frame.contentWindow.scrollTo(0, scrollPos); |
| } catch(e) {} |
| }; |
| } catch(e) { |
| frame.src = url; |
| } |
| } |
| |
| function updateStatus(state, text) { |
| const dot = document.getElementById('status-dot'); |
| const statusText = document.getElementById('status-text'); |
| |
| dot.className = 'status-dot status-' + state; |
| statusText.textContent = text; |
| } |
| |
| function toggleConsole() { |
| isConsoleVisible = !isConsoleVisible; |
| document.getElementById('console-panel').classList.toggle('visible', isConsoleVisible); |
| } |
| |
| function clearConsole() { |
| document.getElementById('console-output').innerHTML = ''; |
| } |
| |
| function reloadPreview() { |
| const frame = document.getElementById('preview-frame'); |
| frame.src = frame.src; |
| } |
| |
| |
| window.addEventListener('message', (event) => { |
| if (event.data.type === 'console') { |
| appendConsole(event.data.level, event.data.args); |
| } |
| }); |
| |
| |
| if ('BroadcastChannel' in window) { |
| const bc = new BroadcastChannel('codemaster_sync'); |
| bc.onmessage = (event) => { |
| if (event.data.type === 'console') { |
| appendConsole(event.data.level, event.data.args); |
| } |
| }; |
| } |
| |
| function appendConsole(level, args) { |
| const output = document.getElementById('console-output'); |
| const line = document.createElement('div'); |
| line.className = 'console-line ' + level; |
| |
| const timestamp = new Date().toLocaleTimeString(); |
| const message = args.join(' '); |
| line.textContent = '[' + timestamp + '] ' + message; |
| |
| output.appendChild(line); |
| output.scrollTop = output.scrollHeight; |
| |
| |
| if (level === 'error' && !isConsoleVisible) { |
| toggleConsole(); |
| } |
| } |
| |
| |
| initSync(); |
| |
| |
| const initial = localStorage.getItem('codemaster_preview'); |
| if (initial) { |
| try { |
| const parsed = JSON.parse(initial); |
| updatePreview(parsed.content); |
| } catch(e) {} |
| } |
| </script> |
| </body> |
| </html> |