maiahmed's picture
🐳 25/03 - 14:34 - .
1e4df54 verified
<!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;
// Try BroadcastChannel first, fallback to localStorage polling
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() });
}
};
// Send ping to check connection
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);
// Also listen for storage events (cross-tab)
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');
// Wrap content to capture console
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);
// Preserve scroll position if possible
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;
}
// Listen for messages from iframe
window.addEventListener('message', (event) => {
if (event.data.type === 'console') {
appendConsole(event.data.level, event.data.args);
}
});
// Listen for BroadcastChannel messages
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;
// Auto-show console on error
if (level === 'error' && !isConsoleVisible) {
toggleConsole();
}
}
// Initialize
initSync();
// Load initial content
const initial = localStorage.getItem('codemaster_preview');
if (initial) {
try {
const parsed = JSON.parse(initial);
updatePreview(parsed.content);
} catch(e) {}
}
</script>
</body>
</html>