Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
46 kB · 1336 lines
HTML
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <script>{const _q=new URLSearchParams(location.search);if(_q.get('wm')==='paper')document.documentElement.classList.add('paperwm');if(_q.get('chrome')==='native')document.documentElement.classList.add('native');}</script> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AC Pane</title>
<style> /* Theme color variables - changes based on system light/dark mode */ :root { /* Dark mode (default) - purple theme */ --ac-border: rgba(136, 68, 255, 0.25); --ac-border-solid: rgba(136, 68, 255, 0.4); --ac-border-hover: rgba(136, 68, 255, 0.5); --ac-border-active: rgba(136, 68, 255, 0.7); --ac-accent: rgba(136, 68, 255, 0.6); --ac-accent-glow: rgba(136, 68, 255, 0.5); --ac-shadow-inner: rgba(136, 68, 255, 0.2); --ac-tab-back: rgba(60, 40, 100, 0.6); --ac-tab-back-hover: rgba(80, 50, 130, 0.8); --ac-tab-back-accent: rgba(100, 60, 160, 0.6); --ac-tab-back-accent-hover: rgba(140, 90, 200, 1); --ac-front-bg: #111; --ac-back-bg: #0a0a12; --ac-text: #fff; --ac-text-muted: #888; } /* Light mode - golden/sand theme (matching legal pad yellow) */ @media (prefers-color-scheme: light) { :root { /* Using #c8a050 (200, 160, 80) as the base golden color */ --ac-border: rgba(200, 160, 80, 0.35); --ac-border-solid: rgba(200, 160, 80, 0.5); --ac-border-hover: rgba(200, 160, 80, 0.65); --ac-border-active: rgba(200, 160, 80, 0.85); --ac-accent: rgba(200, 160, 80, 0.6); --ac-accent-glow: rgba(200, 160, 80, 0.5); --ac-shadow-inner: rgba(200, 160, 80, 0.25); --ac-tab-back: rgba(200, 160, 80, 0.4); --ac-tab-back-hover: rgba(200, 160, 80, 0.6); --ac-tab-back-accent: rgba(200, 160, 80, 0.5); --ac-tab-back-accent-hover: rgba(200, 160, 80, 0.9); --ac-front-bg: #fcf7c5; --ac-back-bg: #f5f0c0; --ac-text: #281e5a; --ac-text-muted: #666; } } * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; background: transparent; font-family: system-ui, sans-serif; user-select: none; -webkit-user-select: none; perspective: 1500px; } /* * 3D FLIP LAYOUT: * - Everything flips together like a physical card * - Desktop shows through during flip (transparent) * - Thick border frames the content * - Side tabs and mode tags flip too (blank backsides) */ /* Perspective container - static, provides 3D context */ .flip-perspective { position: fixed; top: 22px; /* Extra space at top for flip animation headroom */ left: 14px; /* Extra space for wider flip tabs */ right: 14px; /* Extra space for wider flip tabs */ bottom: 36px; /* Extra space at bottom for mode tags + flip headroom */ perspective: 1200px; perspective-origin: center center; } /* The flipping card - this actually rotates in 3D */ .flip-card { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); /* Rotation angle set via JS inline style */ } /* Shared face styles - positioned in 3D space */ .card-face { position: absolute; inset: 0; border: 6px solid var(--ac-border); border-radius: 10px; overflow: hidden; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3), inset 0 0 0 1px var(--ac-shadow-inner); background: transparent; /* No transition - JS controls the swap at midpoint */ } /* Front face - webview - active and interactive when not flipped */ .card-front { z-index: 1; opacity: 1; filter: none; background: var(--ac-front-bg); } /* Midflip class applied by JS at exact midpoint */ body.midflip .card-front { z-index: 3; opacity: 0.15; filter: blur(2px); pointer-events: none; }
.card-front webview { width: 100%; height: 100%; border: none; } /* Back face - dark gray backdrop, ghost ON TOP when not flipped */ .card-back { z-index: 3; transform: rotateY(180deg); opacity: 0.15; filter: blur(2px); pointer-events: none; background: #222; } /* Midflip class - back becomes active */ body.midflip .card-back { z-index: 1; opacity: 1; filter: none; pointer-events: auto; } /* Flip tabs - attached to card edges, swap appearance on flip */ .flip-tab { position: absolute; z-index: 250; width: 26px; top: 50%; height: 80px; cursor: grab; transform: translateY(-50%); /* Don't set app-region here - we need click events on the parent */ }
.flip-tab:hover { cursor: grab; }
body.tab-dragging .flip-tab, body.tab-dragging .flip-tab * { cursor: grabbing !important; }
/* Tab front face - visible when not flipped */ .flip-tab .tab-front { position: absolute; inset: 0; background: var(--ac-border-solid); display: flex; align-items: center; justify-content: center; cursor: inherit; } .flip-tab .tab-front::after { content: ''; width: 3px; height: 30px; background: var(--ac-accent); border-radius: 2px; transition: all 0.2s ease; pointer-events: none; /* Don't block parent clicks */ } .flip-tab:hover .tab-front { background: var(--ac-border-hover); } .flip-tab:hover .tab-front::after { background: var(--ac-border-active); height: 50px; box-shadow: 0 0 12px var(--ac-accent-glow); } /* Tab back face - visible when flipped */ .flip-tab .tab-back { position: absolute; inset: 0; background: var(--ac-tab-back); opacity: 0; display: flex; align-items: center; justify-content: center; cursor: inherit; }
.flip-tab .tab-back::after { content: ''; width: 3px; height: 30px; background: var(--ac-tab-back-accent); border-radius: 2px; transition: all 0.2s ease; pointer-events: none; /* Don't block parent clicks */ } /* Hover effect for back tab */ .flip-tab:hover .tab-back { background: var(--ac-tab-back-hover); } .flip-tab:hover .tab-back::after { background: var(--ac-tab-back-accent-hover); height: 50px; box-shadow: 0 0 12px var(--ac-accent-glow); } /* When midflip, swap front/back visibility */ body.midflip .flip-tab .tab-front { opacity: 0; } body.midflip .flip-tab .tab-back { opacity: 1; } /* Left tab - on left edge */ .flip-tab.left { left: -26px; } .flip-tab.left .tab-front, .flip-tab.left .tab-back { border-radius: 6px 0 0 6px; } /* Right tab - on right edge */ .flip-tab.right { right: -26px; } .flip-tab.right .tab-front, .flip-tab.right .tab-back { border-radius: 0 6px 6px 0; }
/* Volume control - hangs under frame on front side */ .volume-control { position: absolute; left: 50%; top: 100%; transform: translateX(-50%); z-index: 240; height: 15px; display: flex; flex-direction: row; align-items: center; justify-content: center; padding: 1px 6px; border-radius: 0 0 7px 7px; background: var(--ac-border-solid); border: 1px solid var(--ac-border); border-top: none; box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2); transition: opacity 0.12s ease; }
body.midflip .volume-control { opacity: 0; pointer-events: none; }
#volume-slider { -webkit-appearance: none; appearance: none; width: 68px; height: 4px; background: var(--ac-border-solid); border-radius: 2px; cursor: pointer; }
#volume-slider::-webkit-slider-runnable-track { height: 4px; background: transparent; border-radius: 2px; }
#volume-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 11px; height: 11px; border-radius: 50%; background: var(--ac-accent); border: 1px solid var(--ac-border-active); box-shadow: 0 0 8px var(--ac-accent-glow); margin-top: -4px; }
#volume-slider::-moz-range-track { height: 4px; background: transparent; border-radius: 2px; }
#volume-slider::-moz-range-thumb { width: 11px; height: 11px; border-radius: 50%; background: var(--ac-accent); border: 1px solid var(--ac-border-active); box-shadow: 0 0 8px var(--ac-accent-glow); }
/* Resize handles on the card border edge */ .resize-handle { position: fixed; z-index: 600; /* Debug: background: rgba(255,0,0,0.2); */ } /* Positions aligned with flip-perspective: top: 22px, left/right: 14px, bottom: 36px */ .resize-handle.top { top: 16px; left: 8px; right: 8px; height: 12px; cursor: n-resize; }
.resize-handle.bottom { bottom: 30px; left: 8px; right: 8px; height: 12px; cursor: s-resize; }
/* Left side - split into top and bottom sections, leaving gap for flip tab */ .resize-handle.left-top { left: 8px; top: 28px; height: calc(50% - 90px); width: 12px; cursor: w-resize; }
.resize-handle.left-bottom { left: 8px; bottom: 42px; height: calc(50% - 90px); width: 12px; cursor: w-resize; }
/* Right side - split into top and bottom sections, leaving gap for flip tab */ .resize-handle.right-top { right: 8px; top: 28px; height: calc(50% - 90px); width: 12px; cursor: e-resize; }
.resize-handle.right-bottom { right: 8px; bottom: 42px; height: calc(50% - 90px); width: 12px; cursor: e-resize; }
.resize-handle.top-left { top: 16px; left: 8px; width: 16px; height: 16px; cursor: nw-resize; }
.resize-handle.top-right { top: 16px; right: 8px; width: 16px; height: 16px; cursor: ne-resize; }
.resize-handle.bottom-left { bottom: 30px; left: 8px; width: 16px; height: 16px; cursor: sw-resize; }
.resize-handle.bottom-right { bottom: 30px; right: 8px; width: 16px; height: 16px; cursor: se-resize; } /* Overlay to capture mouse during resize drag */ .resize-overlay { position: fixed; inset: 0; z-index: 9999; cursor: inherit; display: none; } .resize-overlay.active { display: block; } /* Spinner cursor during swivel animation */ body.swiveling, body.swiveling * { cursor: wait !important; } /* Disable pointer events on content during resize */ body.resizing .view, body.resizing webview { pointer-events: none !important; }
/* PaperWM: full-bleed webview only, no card chrome, no back face */ html.paperwm { background: #000; } html.paperwm body { perspective: none; } html.paperwm .flip-perspective { top: 0; left: 0; right: 0; bottom: 0; perspective: none; } html.paperwm .flip-card { transform-style: flat; transition: none; } html.paperwm .card-face { border: none; border-radius: 0; box-shadow: none; } html.paperwm .card-back, html.paperwm .flip-tab, html.paperwm .mode-tags, html.paperwm .volume-control, html.paperwm .resize-handle, html.paperwm .resize-overlay { display: none !important; } html.paperwm .card-front { position: absolute; inset: 0; } html.paperwm .card-front webview { width: 100% !important; height: 100% !important; }
/* Native macOS chrome: real OS title bar + traffic lights (hiddenInset), an accent-tinted drag strip across the top, and a full-bleed webview. The custom card frame / flip / resize handles / volume are suppressed. */ html.native { background: var(--ac-front-bg, #111); } html.native body { perspective: none; } html.native .flip-perspective { top: 0; left: 0; right: 0; bottom: 0; /* full-bleed webview under the native title bar */ perspective: none; } html.native .flip-card { transform-style: flat; transition: none; } html.native .card-face { border: none; border-radius: 0; box-shadow: none; } html.native .card-back, html.native .flip-tab, html.native .mode-tags, html.native .volume-control, html.native .resize-handle, html.native .resize-overlay { display: none !important; } html.native .card-front { position: absolute; inset: 0; } html.native .card-front webview { width: 100% !important; height: 100% !important; } /* No custom strip — the standard macOS title bar handles the top. */ .native-topbar { display: none !important; }
</style></head><body> <!-- Native chrome: accent-tinted top strip (drag region; traffic lights overlay left) --> <div class="native-topbar">aesthetic.computer</div>
<!-- Overlay to capture mouse during resize --> <div class="resize-overlay" id="resize-overlay"></div> <!-- Resize handles on the card border (split on sides to avoid flip tabs) --> <div class="resize-handle top" data-resize="top"></div> <div class="resize-handle bottom" data-resize="bottom"></div> <div class="resize-handle left-top" data-resize="left"></div> <div class="resize-handle left-bottom" data-resize="left"></div> <div class="resize-handle right-top" data-resize="right"></div> <div class="resize-handle right-bottom" data-resize="right"></div> <div class="resize-handle top-left" data-resize="top-left"></div> <div class="resize-handle top-right" data-resize="top-right"></div> <div class="resize-handle bottom-left" data-resize="bottom-left"></div> <div class="resize-handle bottom-right" data-resize="bottom-right"></div> <!-- Perspective container --> <div class="flip-perspective"> <!-- The flipping card with front and back faces --> <div class="flip-card"> <!-- Flip tabs - inside card so they swivel with it --> <div class="flip-tab left" data-flip> <div class="tab-front"></div> <div class="tab-back"></div> </div> <div class="flip-tab right" data-flip> <div class="tab-front"></div> <div class="tab-back"></div> </div>
<div class="volume-control" aria-label="Master volume"> <input id="volume-slider" type="range" min="0" max="1" step="0.01" value="0.25" /> </div> <!-- Front face: AC Webview --> <div class="card-face card-front"> <!-- src is set by initWebviewSrc() from ?base=/?piece=. Start blank so a hardcoded URL doesn't commit first and swallow the real assignment. --> <webview id="front-webview" src="about:blank" allowpopups preload="../webview-preload.js" webpreferences="contextIsolation=no"></webview> </div> <!-- Back face: Terminal --> <div class="card-face card-back"></div> </div> </div> <!-- close flip-perspective -->
<script> const isPaperWM = document.documentElement.classList.contains('paperwm'); const { ipcRenderer } = require('electron');
// 🎨 Tint the chrome (frame border, volume, flip tab, glows) to the macOS // system accent color. Backgrounds/text still follow light/dark. Accent // arrives as ?accent=rrggbb on load and via 'ac-accent' IPC on change. function applyAccentChrome(r, g, b) { const root = document.documentElement; const set = (name, a) => root.style.setProperty(name, `rgba(${r}, ${g}, ${b}, ${a})`); set('--ac-border', 0.25); set('--ac-border-solid', 0.4); set('--ac-border-hover', 0.5); set('--ac-border-active', 0.7); set('--ac-accent', 0.6); set('--ac-accent-glow', 0.5); set('--ac-shadow-inner', 0.2); set('--ac-tab-back', 0.45); set('--ac-tab-back-hover', 0.65); set('--ac-tab-back-accent', 0.55); set('--ac-tab-back-accent-hover', 0.9); // Native top strip: a near-solid accent bar. root.style.setProperty('--ac-accent-bar', `rgba(${r}, ${g}, ${b}, 0.92)`); } try { const a = new URLSearchParams(location.search).get('accent'); if (a && /^[0-9a-fA-F]{6}$/.test(a)) { applyAccentChrome(parseInt(a.slice(0, 2), 16), parseInt(a.slice(2, 4), 16), parseInt(a.slice(4, 6), 16)); } } catch (_) {} ipcRenderer.on('ac-accent', (_e, c) => { if (c) applyAccentChrome(c.r, c.g, c.b); });
const flipCard = document.querySelector('.flip-card'); const webviewEl = document.getElementById('front-webview');
// Initial src comes from ?piece= (defaults to 'prompt') and ?base= (defaults to prod). // A <webview> ignores .src assigned before it's attached, and its first // dom-ready (from the about:blank placeholder) can fire before our listener // attaches — a race that left the pane blank. So poll for attachment // (getWebContentsId throws until ready) and navigate the moment it's live. (function initWebviewSrc() { const params = new URLSearchParams(location.search); const piece = (params.get('piece') || 'prompt').replace(/^\/+/, ''); const base = params.get('base') || 'https://aesthetic.computer'; const target = `${base}/${piece}?desktop`; let tries = 0; const iv = setInterval(() => { tries++; let ready = false; try { ready = webviewEl.getWebContentsId() != null; } catch (e) {} if (ready) { webviewEl.src = target; clearInterval(iv); } else if (tries > 60) { clearInterval(iv); // ~6s; give up rather than spin forever } }, 100); })(); const flipTabs = document.querySelectorAll('.flip-tab'); const volumeSlider = document.getElementById('volume-slider'); let showingTerminal = false; let webviewReady = false; let pendingMasterVolume = null;
const DEFAULT_MASTER_VOLUME = 0.25; const VOLUME_STORAGE_KEY = 'ac-master-volume'; const FLIP_ANIMATION_MS = 700; const FLIP_DUCK_FACTOR = 0.35; // Dip 65% while the back pane is active const FLIP_VOLUME_FADE_MS = FLIP_ANIMATION_MS; let appliedMasterVolume = DEFAULT_MASTER_VOLUME; let volumeFadeRaf = null; function clampVolume(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) return DEFAULT_MASTER_VOLUME; return Math.min(1, Math.max(0, numeric)); }
function updateVolumeUI(value) { const clamped = clampVolume(value); volumeSlider.value = clamped.toString(); return clamped; }
function getBaseMasterVolume() { return clampVolume(volumeSlider.value); }
function getEffectiveMasterVolume(baseVolume = getBaseMasterVolume()) { const attenuation = showingTerminal ? FLIP_DUCK_FACTOR : 1; return clampVolume(baseVolume * attenuation); }
function sendMasterVolume(value) { const clamped = clampVolume(value); console.log('[volume] Sending master volume:', clamped); if (typeof webviewEl?.executeJavaScript !== 'function') { pendingMasterVolume = clamped; return false; } const code = `(() => { const v = ${clamped}; console.log('[AC] setMasterVolume called with:', v); if (window.AC && typeof window.AC.setMasterVolume === 'function') { const result = window.AC.setMasterVolume(v); console.log('[AC] setMasterVolume result:', result); return { success: true, method: 'AC.setMasterVolume', result }; } if (typeof window.acSetMasterVolume === 'function') { const result = window.acSetMasterVolume(v); console.log('[AC] acSetMasterVolume result:', result); return { success: true, method: 'acSetMasterVolume', result }; } console.warn('[AC] No volume setter found. AC:', window.AC); return { success: false, AC: !!window.AC }; })()`; webviewEl.executeJavaScript(code, true) .then(result => console.log('[volume] executeJavaScript result:', result)) .catch(err => console.error('[volume] executeJavaScript error:', err)); pendingMasterVolume = null; return true; }
function applyMasterVolume(value) { const clamped = clampVolume(value); appliedMasterVolume = clamped; if (!webviewReady) { pendingMasterVolume = clamped; return false; } return sendMasterVolume(clamped); }
function fadeMasterVolumeTo(value, durationMs = FLIP_VOLUME_FADE_MS) { const target = clampVolume(value);
if (volumeFadeRaf !== null) { cancelAnimationFrame(volumeFadeRaf); volumeFadeRaf = null; }
if (!webviewReady || durationMs <= 0) { applyMasterVolume(target); return; }
const startValue = appliedMasterVolume; const delta = target - startValue; if (Math.abs(delta) < 0.001) { applyMasterVolume(target); return; }
const startTime = performance.now(); const step = (now) => { const t = Math.min(1, (now - startTime) / durationMs); const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; applyMasterVolume(startValue + (delta * eased));
if (t < 1) { volumeFadeRaf = requestAnimationFrame(step); return; } volumeFadeRaf = null; applyMasterVolume(target); };
volumeFadeRaf = requestAnimationFrame(step); }
function syncMasterVolume({ animate = false, durationMs = FLIP_VOLUME_FADE_MS } = {}) { const baseVolume = getBaseMasterVolume(); sessionStorage.setItem(VOLUME_STORAGE_KEY, String(baseVolume)); const effectiveVolume = getEffectiveMasterVolume(baseVolume); if (animate) { fadeMasterVolumeTo(effectiveVolume, durationMs); return; } applyMasterVolume(effectiveVolume); }
// Initialize per-window volume const storedVolume = sessionStorage.getItem(VOLUME_STORAGE_KEY); if (storedVolume !== null) { updateVolumeUI(storedVolume); } else { updateVolumeUI(DEFAULT_MASTER_VOLUME); sessionStorage.setItem(VOLUME_STORAGE_KEY, String(DEFAULT_MASTER_VOLUME)); } appliedMasterVolume = getEffectiveMasterVolume(); syncMasterVolume(); // Extract piece name from URL function extractPieceName(url) { try { const parsed = new URL(url); const pathname = parsed.pathname; // Remove leading slash and get first segment const piece = pathname.replace(/^\//, '').split('/')[0] || 'prompt'; return piece; } catch (e) { return 'prompt'; } } // Track navigation and update tray title webviewEl.addEventListener('did-navigate', (e) => { const piece = extractPieceName(e.url); console.log('[flip] Navigated to piece:', piece); ipcRenderer.invoke('set-current-piece', piece); syncMasterVolume(); }); webviewEl.addEventListener('did-navigate-in-page', (e) => { if (e.isMainFrame) { const piece = extractPieceName(e.url); console.log('[flip] In-page navigation to piece:', piece); // Intercept 'local' and 'prod' to switch servers if (piece === 'local' || piece === 'prod') { const base = piece === 'local' ? 'http://localhost:8888' : 'https://aesthetic.computer'; console.log(`[flip] Switching to ${piece} server: ${base}`); webviewEl.src = `${base}/prompt?desktop`; return; } ipcRenderer.invoke('set-current-piece', piece); syncMasterVolume(); } });
webviewEl.addEventListener('dom-ready', () => { console.log('[volume] dom-ready - syncing volume') webviewReady = true; // 🎮 Grant the guest renderer sticky user activation so its own // navigator.getGamepads() returns real pads. Chromium gates the // Gamepad API behind sticky activation; without this, pads are // invisible until the user clicks inside the webview. try { webviewEl.executeJavaScript('1', true); } catch (_) {} if (pendingMasterVolume !== null) { appliedMasterVolume = clampVolume(pendingMasterVolume); sendMasterVolume(appliedMasterVolume); } syncMasterVolume(); // Retry after AC has had time to boot setTimeout(() => { console.log('[volume] Retry sync after 2s delay'); syncMasterVolume(); }, 2000); setTimeout(() => { console.log('[volume] Retry sync after 5s delay'); syncMasterVolume(); }, 5000); });
// 🎮 Gamepad bridge — host → guest. // Inside an Electron <webview>, navigator.getGamepads() in the guest // renderer often stays empty (the guest doesn't reliably receive the // user activation Chromium gates the API behind). Poll at the host, // which has activation as soon as the BrowserWindow is interacted // with (or via the executeJavaScript('1', true) call from main.js), // and forward a snapshot to the webview where webview-preload.js // surfaces it via a patched navigator.getGamepads. (function setupGamepadBridge() { let lastSig = ''; let loggedSeen = false; let loggedConnect = new Set(); window.addEventListener('gamepadconnected', (e) => { console.log('[flip] gamepadconnected at host:', e.gamepad?.id, 'index', e.gamepad?.index); }); window.addEventListener('gamepaddisconnected', (e) => { console.log('[flip] gamepaddisconnected at host:', e.gamepad?.id, 'index', e.gamepad?.index); }); setInterval(() => { if (!webviewEl || !webviewReady) return; const pads = navigator.getGamepads ? navigator.getGamepads() : []; const snapshot = []; let any = false; for (let i = 0; i < pads.length; i++) { const g = pads[i]; if (!g) { snapshot.push(null); continue; } any = true; snapshot.push({ index: g.index, id: g.id, connected: g.connected, timestamp: g.timestamp, mapping: g.mapping, axes: Array.from(g.axes), buttons: Array.from(g.buttons).map((b) => ({ pressed: b.pressed, touched: b.touched, value: b.value, })), }); } const sig = JSON.stringify(snapshot); if (sig === lastSig) return; lastSig = sig; if (any && !loggedSeen) { loggedSeen = true; const first = snapshot.find(Boolean); console.log('[flip] gamepad bridge: host detected pad, forwarding to webview:', first?.id); } for (const p of snapshot) { if (p && !loggedConnect.has(p.index)) { loggedConnect.add(p.index); console.log('[flip] gamepad bridge: forwarding new pad index', p.index, p.id); } } try { webviewEl.send('ac:gamepad-state', snapshot); } catch (_) {} }, 16); })(); // Handle IPC from main process to open devtools ipcRenderer.on('open-devtools', () => { console.log('[flip] Received open-devtools from main'); webviewEl.openDevTools(); }); // Handle context menu on webview (right-click) webviewEl.addEventListener('context-menu', (e) => { console.log('[flip] Webview context-menu event', e.params); e.preventDefault(); showContextMenu(e.params.x, e.params.y); }); // Handle IPC messages from webview preload (the primary communication channel) webviewEl.addEventListener('ipc-message', (e) => { const channel = e.channel; const args = e.args?.[0] || {}; console.log('[flip] ipc-message from webview:', channel, args); if (channel === 'ac-open-window') { const { url, index = 0, total = 1 } = args; console.log('[flip] Opening new window via ipc-message:', url, index, total); ipcRenderer.invoke('ac-open-window', { url, index, total }); } else if (channel === 'ac-close-window') { console.log('[flip] Closing window via ipc-message'); ipcRenderer.invoke('ac-close-window'); } }); // Handle window.open() from webview (fallback for older code paths) // Note: Main process also handles this via setWindowOpenHandler webviewEl.addEventListener('new-window', (e) => { console.log('[flip] new-window event received:', e?.url, e); if (e?.preventDefault) e.preventDefault(); const url = e?.url || ''; if (url.startsWith('ac://close')) { console.log('[flip] Handling ac://close - invoking ac-close-window'); ipcRenderer.invoke('ac-close-window'); return; } // Check if this is an external URL that should open in the system browser try { const urlObj = new URL(url); const isExternal = !urlObj.hostname.includes('aesthetic.computer') && !urlObj.hostname.includes('localhost') && !urlObj.hostname.includes('127.0.0.1') && !url.startsWith('ac://'); if (isExternal) { console.log('[flip] Opening external URL in system browser:', url); ipcRenderer.invoke('open-external-url', url); return; } } catch (err) { console.warn('[flip] Failed to parse URL:', url, err.message); } console.log('[flip] Opening new window with url:', url); ipcRenderer.invoke('ac-open-window', { url, index: 0 }); }); // Listen for postMessage from webview (legacy fallback) window.addEventListener('message', (e) => { if (e.data?.type === 'ac-open-window') { console.log('[flip] Received ac-open-window postMessage:', e.data); ipcRenderer.invoke('ac-open-window', { url: e.data.url, index: e.data.index || 0, total: e.data.total || 1 }); } else if (e.data?.type === 'ac-close-window') { console.log('[flip] Received ac-close-window postMessage'); ipcRenderer.invoke('ac-close-window'); } }); // Listen for navigate commands from main process (for new windows) ipcRenderer.on('navigate', (event, url) => { console.log('[flip] Received navigate command:', url); if (url && webviewEl) { webviewEl.src = url; } });
// 🎵 Dropped audio file relay (main → host → webview → AC page). // Main process registers the file with its loopback server and pushes // the resulting localhost url through IPC. We forward it into the // webview so webview-preload.js can postMessage it to the AC page. let pendingDroppedFile = null; ipcRenderer.on('ac-dropped-file', (_event, payload) => { console.log('[flip] ac-dropped-file:', payload?.name, payload?.url); if (webviewReady) { try { webviewEl.send('ac:dropped-file', payload); } catch (e) {} } // Always cache the most recent payload — webview's dom-ready may fire // again after a piece-load nav and we want a re-send for that, too. pendingDroppedFile = payload; });
// Replay on every webview dom-ready (covers cold launch and piece nav). webviewEl.addEventListener('dom-ready', () => { if (pendingDroppedFile) { try { webviewEl.send('ac:dropped-file', pendingDroppedFile); } catch (e) {} // Consume after first delivery; a fresh drop refills it. pendingDroppedFile = null; } });
// 🎵 HTML5 drag-drop onto the running window. The Electron host page can // see File.path on dropped files; we hand the path to main and it does // the rest (registers with the loopback server and broadcasts). function acIsAudioName(name) { return /\.(mp3|wav|flac|ogg|m4a|aac|opus)$/i.test(name || ''); } window.addEventListener('dragover', (e) => { const has = Array.from(e.dataTransfer?.items || []) .some((it) => it.kind === 'file'); if (!has) return; e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }); window.addEventListener('drop', (e) => { const files = Array.from(e.dataTransfer?.files || []); if (!files.length) return; e.preventDefault(); for (const f of files) { if (f.path && acIsAudioName(f.name)) { ipcRenderer.invoke('ac:register-dropped-file', f.path) .catch((err) => console.warn('[flip] register-dropped-file failed:', err)); } } }); // Also try did-create-window (Electron 22+) webviewEl.addEventListener('did-create-window', (e) => { console.log('[flip] did-create-window event:', e); }); // And will-navigate for debugging webviewEl.addEventListener('will-navigate', (e) => { console.log('[flip] will-navigate:', e?.url); }); // Mark this as the main window and set initial piece ipcRenderer.invoke('set-main-window'); ipcRenderer.invoke('set-current-piece', 'prompt'); volumeSlider.addEventListener('input', () => { updateVolumeUI(volumeSlider.value); syncMasterVolume(); }); // Listen for global flip shortcut from main process ipcRenderer.on('toggle-flip', () => { toggle(); }); // Listen for navigate messages (from new window requests) ipcRenderer.on('navigate', (event, url) => { console.log('[flip] Navigate to:', url); if (url && typeof url === 'string') { webviewEl.src = url; } }); // ========== Zoom Handling ========== let frontZoom = 1.0;
ipcRenderer.on('zoom-in', () => { frontZoom = Math.min(frontZoom + 0.1, 3.0); webviewEl.setZoomFactor(frontZoom); });
ipcRenderer.on('zoom-out', () => { frontZoom = Math.max(frontZoom - 0.1, 0.3); webviewEl.setZoomFactor(frontZoom); });
ipcRenderer.on('zoom-reset', () => { frontZoom = 1.0; webviewEl.setZoomFactor(frontZoom); });
// ========== Toggle Logic ========== let rotationAngle = 0; // Track cumulative rotation let midflipTimeout = null; let swivelingTimeout = null; function toggle(direction = 'right') { console.log('[flip] toggle() called, direction:', direction, 'rotation was:', rotationAngle); // Clear any pending timeouts if (midflipTimeout) { clearTimeout(midflipTimeout); } if (swivelingTimeout) { clearTimeout(swivelingTimeout); } // Show spinner cursor during swivel document.body.classList.add('swiveling'); // Always rotate 180deg in the specified direction (continuous spin) // Right tab = clockwise (positive), Left tab = counter-clockwise (negative) if (direction === 'right') { rotationAngle += 180; } else { rotationAngle -= 180; } // Apply rotation directly to flip-card flipCard.style.transform = `rotateY(${rotationAngle}deg)`; // Determine which side will be showing after flip completes const normalized = ((rotationAngle % 360) + 360) % 360; const willShowTerminal = (normalized > 90 && normalized < 270); const targetFlipVolume = clampVolume(getBaseMasterVolume() * (willShowTerminal ? FLIP_DUCK_FACTOR : 1));
// Fade ducking/unducking across the same duration as the flip animation. fadeMasterVolumeTo(targetFlipVolume, FLIP_ANIMATION_MS); // Swap translucency when card is edge-on (90°) // With cubic-bezier(0.4, 0, 0.2, 1) over 700ms, 90° is reached around 260ms midflipTimeout = setTimeout(() => { showingTerminal = willShowTerminal; document.body.classList.toggle('midflip', showingTerminal); document.body.classList.toggle('flipped', showingTerminal); console.log('[flip] Edge-on reached - swapping translucency, showingTerminal:', showingTerminal); }, 260); // Remove spinner cursor when swivel completes swivelingTimeout = setTimeout(() => { document.body.classList.remove('swiveling'); }, FLIP_ANIMATION_MS); console.log('[flip] Will show', willShowTerminal ? 'back' : 'front', 'rotation:', rotationAngle, 'normalized:', normalized); } // Flip tabs - drag to move window, click to flip console.log('[flip] Found flip tabs:', flipTabs.length); flipTabs.forEach(tab => { tab.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation();
const mouseDownTime = Date.now(); const startClient = { x: e.clientX, y: e.clientY }; const startScreen = { x: e.screenX, y: e.screenY }; const startWindow = { x: window.screenX, y: window.screenY }; let hasDragged = false;
document.body.classList.add('tab-dragging');
const onMouseMove = (moveEvent) => { const dx = moveEvent.screenX - startScreen.x; const dy = moveEvent.screenY - startScreen.y; if (!hasDragged && Math.hypot(dx, dy) >= 4) { hasDragged = true; } if (hasDragged) { ipcRenderer.send('move-window', { x: startWindow.x + dx, y: startWindow.y + dy, }); } };
const onMouseUp = (upEvent) => { document.body.classList.remove('tab-dragging'); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp);
if (hasDragged) return;
const timeDiff = Date.now() - mouseDownTime; const moveDiff = Math.hypot(upEvent.clientX - startClient.x, upEvent.clientY - startClient.y); if (timeDiff < 300 && moveDiff < 10) { // Base direction on SCREEN position, not tab class const windowCenter = window.innerWidth / 2; const direction = (upEvent.clientX > windowCenter) ? 'right' : 'left'; console.log('[flip] Flip tab clicked at x:', upEvent.clientX, 'center:', windowCenter, 'direction:', direction); toggle(direction); } };
document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Tab to toggle flip if (e.key === 'Tab') { console.log('[flip] Tab key pressed'); e.preventDefault(); toggle(); } // Cmd+Shift+I or Ctrl+Shift+I to open webview devtools (check both cases) if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 'i' || e.key === 'I')) { e.preventDefault(); console.log('[flip] Opening webview DevTools via keyboard'); if (webviewEl.isDevToolsOpened()) { webviewEl.closeDevTools(); } else { webviewEl.openDevTools(); } } // F12 also opens devtools (Windows standard) if (e.key === 'F12') { e.preventDefault(); console.log('[flip] Opening webview DevTools via F12'); if (webviewEl.isDevToolsOpened()) { webviewEl.closeDevTools(); } else { webviewEl.openDevTools(); } } }); // Context menu for DevTools document.addEventListener('contextmenu', (e) => { // Show custom context menu e.preventDefault(); showContextMenu(e.clientX, e.clientY); }); // Custom context menu function showContextMenu(x, y) { // Remove any existing menu const existing = document.getElementById('custom-context-menu'); if (existing) existing.remove(); const menu = document.createElement('div'); menu.id = 'custom-context-menu'; menu.style.cssText = ` position: fixed; left: ${x}px; top: ${y}px; background: rgba(30, 30, 40, 0.95); border: 1px solid var(--ac-border); border-radius: 6px; padding: 4px 0; min-width: 160px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); z-index: 10000; font-size: 12px; backdrop-filter: blur(8px); `; const menuItems = [ { label: '🔧 Open DevTools', action: () => webviewEl.openDevTools() }, { label: '🔄 Reload Page', action: () => webviewEl.reload() }, { label: '📋 Copy URL', action: () => navigator.clipboard.writeText(webviewEl.src) }, { type: 'separator' }, { label: '🔊 Reset Volume', action: () => { updateVolumeUI(DEFAULT_MASTER_VOLUME); syncMasterVolume(); } }, ]; menuItems.forEach(item => { if (item.type === 'separator') { const sep = document.createElement('div'); sep.style.cssText = 'height: 1px; background: var(--ac-border); margin: 4px 8px;'; menu.appendChild(sep); } else { const menuItem = document.createElement('div'); menuItem.textContent = item.label; menuItem.style.cssText = ` padding: 6px 12px; cursor: pointer; color: var(--ac-text); `; menuItem.addEventListener('mouseenter', () => { menuItem.style.background = 'var(--ac-border-hover)'; }); menuItem.addEventListener('mouseleave', () => { menuItem.style.background = 'transparent'; }); menuItem.addEventListener('click', () => { item.action(); menu.remove(); }); menu.appendChild(menuItem); } }); document.body.appendChild(menu); // Close menu on click outside const closeMenu = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', closeMenu); } }; setTimeout(() => document.addEventListener('click', closeMenu), 0); } // ========== Custom Resize Handles ========== const resizeOverlay = document.getElementById('resize-overlay'); document.querySelectorAll('[data-resize]').forEach(handle => { handle.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); const direction = handle.dataset.resize; // Get cursor style for this direction const cursorStyle = window.getComputedStyle(handle).cursor; // Show overlay and set body class to disable pointer events on content resizeOverlay.style.cursor = cursorStyle; resizeOverlay.classList.add('active'); document.body.classList.add('resizing'); const startX = e.screenX; const startY = e.screenY; const startBounds = { x: window.screenX, y: window.screenY, width: window.outerWidth, height: window.outerHeight }; const onMouseMove = (e) => { const dx = e.screenX - startX; const dy = e.screenY - startY; let newX = startBounds.x; let newY = startBounds.y; let newWidth = startBounds.width; let newHeight = startBounds.height; if (direction.includes('left')) { newX = startBounds.x + dx; newWidth = startBounds.width - dx; } if (direction.includes('right')) { newWidth = startBounds.width + dx; } if (direction.includes('top')) { newY = startBounds.y + dy; newHeight = startBounds.height - dy; } if (direction.includes('bottom')) { newHeight = startBounds.height + dy; } // Minimum size if (newWidth < 400) { newWidth = 400; newX = startBounds.x + startBounds.width - 400; } if (newHeight < 300) { newHeight = 300; newY = startBounds.y + startBounds.height - 300; } ipcRenderer.send('resize-window', { x: newX, y: newY, width: newWidth, height: newHeight }); }; const onMouseUp = () => { // Hide overlay and restore pointer events resizeOverlay.classList.remove('active'); document.body.classList.remove('resizing'); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); }); // ========== Alt + Scroll to Drag Window ========== let scrollDragActive = false; let scrollDragStart = { x: 0, y: 0 }; document.addEventListener('wheel', (e) => { // Alt + scroll (two-finger drag with alt) moves the window if (e.altKey) { e.preventDefault(); // Use deltaX and deltaY to move window const currentX = window.screenX; const currentY = window.screenY; // Invert and scale the delta for natural dragging feel const newX = currentX - e.deltaX; const newY = currentY - e.deltaY; ipcRenderer.send('move-window', { x: Math.round(newX), y: Math.round(newY) }); } }, { passive: false }); </script></body></html>