Monorepo for Aesthetic.Computer aesthetic.computer
at main 1645 lines 53 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <script>if(new URLSearchParams(location.search).get('wm')==='paper')document.documentElement.classList.add('paperwm')</script> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>AC Pane</title> 8 <link rel="stylesheet" href="../node_modules/@xterm/xterm/css/xterm.css"> 9 <style> 10 /* Theme color variables - changes based on system light/dark mode */ 11 :root { 12 /* Dark mode (default) - purple theme */ 13 --ac-border: rgba(136, 68, 255, 0.25); 14 --ac-border-solid: rgba(136, 68, 255, 0.4); 15 --ac-border-hover: rgba(136, 68, 255, 0.5); 16 --ac-border-active: rgba(136, 68, 255, 0.7); 17 --ac-accent: rgba(136, 68, 255, 0.6); 18 --ac-accent-glow: rgba(136, 68, 255, 0.5); 19 --ac-shadow-inner: rgba(136, 68, 255, 0.2); 20 --ac-tab-back: rgba(60, 40, 100, 0.6); 21 --ac-tab-back-hover: rgba(80, 50, 130, 0.8); 22 --ac-tab-back-accent: rgba(100, 60, 160, 0.6); 23 --ac-tab-back-accent-hover: rgba(140, 90, 200, 1); 24 --ac-front-bg: #111; 25 --ac-back-bg: #0a0a12; 26 --ac-text: #fff; 27 --ac-text-muted: #888; 28 } 29 30 /* Light mode - golden/sand theme (matching legal pad yellow) */ 31 @media (prefers-color-scheme: light) { 32 :root { 33 /* Using #c8a050 (200, 160, 80) as the base golden color */ 34 --ac-border: rgba(200, 160, 80, 0.35); 35 --ac-border-solid: rgba(200, 160, 80, 0.5); 36 --ac-border-hover: rgba(200, 160, 80, 0.65); 37 --ac-border-active: rgba(200, 160, 80, 0.85); 38 --ac-accent: rgba(200, 160, 80, 0.6); 39 --ac-accent-glow: rgba(200, 160, 80, 0.5); 40 --ac-shadow-inner: rgba(200, 160, 80, 0.25); 41 --ac-tab-back: rgba(200, 160, 80, 0.4); 42 --ac-tab-back-hover: rgba(200, 160, 80, 0.6); 43 --ac-tab-back-accent: rgba(200, 160, 80, 0.5); 44 --ac-tab-back-accent-hover: rgba(200, 160, 80, 0.9); 45 --ac-front-bg: #fcf7c5; 46 --ac-back-bg: #f5f0c0; 47 --ac-text: #281e5a; 48 --ac-text-muted: #666; 49 } 50 } 51 52 * { margin: 0; padding: 0; box-sizing: border-box; } 53 html, body { 54 width: 100%; 55 height: 100%; 56 overflow: hidden; 57 background: transparent; 58 font-family: system-ui, sans-serif; 59 user-select: none; 60 -webkit-user-select: none; 61 perspective: 1500px; 62 } 63 64 /* 65 * 3D FLIP LAYOUT: 66 * - Everything flips together like a physical card 67 * - Desktop shows through during flip (transparent) 68 * - Thick border frames the content 69 * - Side tabs and mode tags flip too (blank backsides) 70 */ 71 72 /* Perspective container - static, provides 3D context */ 73 .flip-perspective { 74 position: fixed; 75 top: 40px; /* Extra space at top for flip animation headroom */ 76 left: 30px; /* Extra space for wider flip tabs */ 77 right: 30px; /* Extra space for wider flip tabs */ 78 bottom: 56px; /* Extra space at bottom for mode tags + flip headroom */ 79 perspective: 1200px; 80 perspective-origin: center center; 81 } 82 83 /* The flipping card - this actually rotates in 3D */ 84 .flip-card { 85 position: absolute; 86 inset: 0; 87 transform-style: preserve-3d; 88 transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); 89 /* Rotation angle set via JS inline style */ 90 } 91 92 /* Shared face styles - positioned in 3D space */ 93 .card-face { 94 position: absolute; 95 inset: 0; 96 border: 6px solid var(--ac-border); 97 border-radius: 10px; 98 overflow: hidden; 99 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3), 100 inset 0 0 0 1px var(--ac-shadow-inner); 101 background: transparent; 102 /* No transition - JS controls the swap at midpoint */ 103 } 104 105 /* Front face - webview - active and interactive when not flipped */ 106 .card-front { 107 z-index: 1; 108 opacity: 1; 109 filter: none; 110 background: var(--ac-front-bg); 111 } 112 113 /* Midflip class applied by JS at exact midpoint */ 114 body.midflip .card-front { 115 z-index: 3; 116 opacity: 0.15; 117 filter: blur(2px); 118 pointer-events: none; 119 } 120 121 .card-front webview { 122 width: 100%; 123 height: 100%; 124 border: none; 125 } 126 127 /* Back face - terminal - ghost ON TOP when not flipped */ 128 .card-back { 129 z-index: 3; 130 transform: rotateY(180deg); 131 opacity: 0.15; 132 filter: blur(2px); 133 pointer-events: none; 134 background: var(--ac-back-bg); 135 } 136 137 /* Midflip class - back becomes active */ 138 body.midflip .card-back { 139 z-index: 1; 140 opacity: 1; 141 filter: none; 142 pointer-events: auto; 143 } 144 145 #terminal-container { 146 width: 100%; 147 height: 100%; 148 padding: 12px; 149 } 150 151 .xterm { height: 100%; } 152 .xterm-viewport { padding-top: 0 !important; } 153 154 /* Flip tabs - attached to card edges, swap appearance on flip */ 155 .flip-tab { 156 position: absolute; 157 z-index: 250; 158 width: 26px; 159 top: 50%; 160 height: 80px; 161 cursor: pointer; 162 transform: translateY(-50%); 163 /* Don't set app-region here - we need click events on the parent */ 164 } 165 166 .flip-tab:hover { 167 cursor: pointer; 168 } 169 170 /* Tab front face - visible when not flipped */ 171 .flip-tab .tab-front { 172 position: absolute; 173 inset: 0; 174 background: var(--ac-border-solid); 175 display: flex; 176 align-items: center; 177 justify-content: center; 178 cursor: pointer; 179 } 180 181 .flip-tab .tab-front::after { 182 content: ''; 183 width: 3px; 184 height: 30px; 185 background: var(--ac-accent); 186 border-radius: 2px; 187 transition: all 0.2s ease; 188 pointer-events: none; /* Don't block parent clicks */ 189 } 190 191 .flip-tab:hover .tab-front { 192 background: var(--ac-border-hover); 193 } 194 195 .flip-tab:hover .tab-front::after { 196 background: var(--ac-border-active); 197 height: 50px; 198 box-shadow: 0 0 12px var(--ac-accent-glow); 199 } 200 201 /* Tab back face - visible when flipped */ 202 .flip-tab .tab-back { 203 position: absolute; 204 inset: 0; 205 background: var(--ac-tab-back); 206 opacity: 0; 207 display: flex; 208 align-items: center; 209 justify-content: center; 210 cursor: pointer; 211 } 212 213 .flip-tab .tab-back::after { 214 content: ''; 215 width: 3px; 216 height: 30px; 217 background: var(--ac-tab-back-accent); 218 border-radius: 2px; 219 transition: all 0.2s ease; 220 pointer-events: none; /* Don't block parent clicks */ 221 } 222 223 /* Hover effect for back tab */ 224 .flip-tab:hover .tab-back { 225 background: var(--ac-tab-back-hover); 226 } 227 228 .flip-tab:hover .tab-back::after { 229 background: var(--ac-tab-back-accent-hover); 230 height: 50px; 231 box-shadow: 0 0 12px var(--ac-accent-glow); 232 } 233 234 /* When midflip, swap front/back visibility */ 235 body.midflip .flip-tab .tab-front { 236 opacity: 0; 237 } 238 239 body.midflip .flip-tab .tab-back { 240 opacity: 1; 241 } 242 243 /* Left tab - on left edge */ 244 .flip-tab.left { 245 left: -26px; 246 } 247 248 .flip-tab.left .tab-front, 249 .flip-tab.left .tab-back { 250 border-radius: 6px 0 0 6px; 251 } 252 253 /* Right tab - on right edge */ 254 .flip-tab.right { 255 right: -26px; 256 } 257 258 .flip-tab.right .tab-front, 259 .flip-tab.right .tab-back { 260 border-radius: 0 6px 6px 0; 261 } 262 263 /* Volume control - integrated into top-right corner of frame */ 264 .volume-control { 265 position: absolute; 266 right: 0; 267 top: -26px; 268 z-index: 240; 269 height: 26px; 270 display: flex; 271 flex-direction: row; 272 align-items: center; 273 gap: 6px; 274 padding: 4px 12px 4px 10px; 275 border-radius: 10px 10px 0 0; 276 background: var(--ac-border-solid); 277 border: 1px solid var(--ac-border); 278 border-bottom: none; 279 box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.2); 280 } 281 282 body.midflip .volume-control { 283 background: var(--ac-tab-back); 284 border-color: var(--ac-border-solid); 285 } 286 287 #volume-slider { 288 -webkit-appearance: none; 289 appearance: none; 290 width: 72px; 291 height: 4px; 292 background: var(--ac-border-solid); 293 border-radius: 2px; 294 cursor: pointer; 295 } 296 297 #volume-slider::-webkit-slider-runnable-track { 298 height: 4px; 299 background: transparent; 300 border-radius: 2px; 301 } 302 303 #volume-slider::-webkit-slider-thumb { 304 -webkit-appearance: none; 305 appearance: none; 306 width: 11px; 307 height: 11px; 308 border-radius: 50%; 309 background: var(--ac-accent); 310 border: 1px solid var(--ac-border-active); 311 box-shadow: 0 0 8px var(--ac-accent-glow); 312 margin-top: -4px; 313 } 314 315 #volume-slider::-moz-range-track { 316 height: 4px; 317 background: transparent; 318 border-radius: 2px; 319 } 320 321 #volume-slider::-moz-range-thumb { 322 width: 11px; 323 height: 11px; 324 border-radius: 50%; 325 background: var(--ac-accent); 326 border: 1px solid var(--ac-border-active); 327 box-shadow: 0 0 8px var(--ac-accent-glow); 328 } 329 330 /* Backend controls - appears on backend side (counter-rotated to fix mirroring) */ 331 .backend-controls { 332 position: absolute; 333 z-index: 100; 334 bottom: -32px; 335 left: 50%; 336 transform: translateX(-50%) rotateY(180deg); 337 opacity: 0; 338 pointer-events: none; 339 transition: opacity 0.1s ease; 340 display: flex; 341 gap: 4px; 342 } 343 344 body.midflip .backend-controls { 345 opacity: 1; 346 pointer-events: auto; 347 } 348 349 .backend-controls button { 350 font-size: 16px; 351 font-weight: 700; 352 padding: 4px 16px 6px 16px; 353 border-radius: 0 0 6px 6px; 354 border: none; 355 cursor: pointer; 356 transition: all 0.2s ease; 357 } 358 359 .backend-controls button.start-btn { 360 background: var(--ac-border-hover); 361 color: var(--ac-text); 362 } 363 364 .backend-controls button.start-btn:hover { 365 background: var(--ac-border-active); 366 box-shadow: 0 2px 12px var(--ac-accent-glow); 367 } 368 369 .backend-controls button.start-btn:disabled { 370 background: var(--ac-border); 371 color: var(--ac-text-muted); 372 cursor: not-allowed; 373 } 374 375 .backend-controls button.stop-btn { 376 background: var(--ac-accent); 377 color: var(--ac-text); 378 } 379 380 .backend-controls button.stop-btn:hover { 381 background: var(--ac-border-active); 382 box-shadow: 0 2px 12px var(--ac-accent-glow); 383 } 384 385 .backend-controls button.stop-btn:disabled { 386 background: var(--ac-border); 387 color: var(--ac-text-muted); 388 cursor: not-allowed; 389 } 390 391 392 /* Resize handles on the card border edge */ 393 .resize-handle { 394 position: fixed; 395 z-index: 600; 396 /* Debug: background: rgba(255,0,0,0.2); */ 397 } 398 399 /* Positions aligned with flip-perspective: top: 40px, left/right: 30px, bottom: 56px */ 400 .resize-handle.top { 401 top: 34px; left: 24px; right: 24px; height: 12px; 402 cursor: n-resize; 403 } 404 405 .resize-handle.bottom { 406 bottom: 50px; left: 24px; right: 24px; height: 12px; 407 cursor: s-resize; 408 } 409 410 /* Left side - split into top and bottom sections, leaving gap for flip tab */ 411 .resize-handle.left-top { 412 left: 24px; top: 46px; height: calc(50% - 90px); width: 12px; 413 cursor: w-resize; 414 } 415 416 .resize-handle.left-bottom { 417 left: 24px; bottom: 62px; height: calc(50% - 90px); width: 12px; 418 cursor: w-resize; 419 } 420 421 /* Right side - split into top and bottom sections, leaving gap for flip tab */ 422 .resize-handle.right-top { 423 right: 24px; top: 46px; height: calc(50% - 90px); width: 12px; 424 cursor: e-resize; 425 } 426 427 .resize-handle.right-bottom { 428 right: 24px; bottom: 62px; height: calc(50% - 90px); width: 12px; 429 cursor: e-resize; 430 } 431 432 .resize-handle.top-left { 433 top: 34px; left: 24px; width: 16px; height: 16px; 434 cursor: nw-resize; 435 } 436 437 .resize-handle.top-right { 438 top: 34px; right: 24px; width: 16px; height: 16px; 439 cursor: ne-resize; 440 } 441 442 .resize-handle.bottom-left { 443 bottom: 50px; left: 24px; width: 16px; height: 16px; 444 cursor: sw-resize; 445 } 446 447 .resize-handle.bottom-right { 448 bottom: 50px; right: 24px; width: 16px; height: 16px; 449 cursor: se-resize; 450 } 451 452 /* Overlay to capture mouse during resize drag */ 453 .resize-overlay { 454 position: fixed; 455 inset: 0; 456 z-index: 9999; 457 cursor: inherit; 458 display: none; 459 } 460 461 /* Backend welcome screen */ 462 .backend-welcome { 463 position: absolute; 464 inset: 0; 465 background: linear-gradient(135deg, #0a0a18 0%, #12081f 50%, #0a0a18 100%); 466 display: flex; 467 flex-direction: column; 468 align-items: center; 469 justify-content: center; 470 padding: 24px; 471 z-index: 10; 472 color: #fff; 473 font-family: system-ui, -apple-system, sans-serif; 474 } 475 476 .backend-welcome.hidden { 477 display: none; 478 } 479 480 .backend-welcome h1 { 481 font-size: 18px; 482 font-weight: 600; 483 margin-bottom: 8px; 484 color: #f0f; 485 text-shadow: 0 0 20px rgba(255, 0, 255, 0.5); 486 } 487 488 .backend-welcome .subtitle { 489 font-size: 11px; 490 color: #888; 491 margin-bottom: 24px; 492 } 493 494 .backend-welcome .hint-text { 495 font-size: 10px; 496 color: #666; 497 margin-top: 16px; 498 padding: 8px 16px; 499 background: rgba(136, 68, 255, 0.1); 500 border-radius: 4px; 501 } 502 503 .backend-welcome .info-section { 504 width: 100%; 505 max-width: 400px; 506 margin-bottom: 20px; 507 } 508 509 .backend-welcome .info-row { 510 display: flex; 511 justify-content: space-between; 512 align-items: center; 513 padding: 8px 12px; 514 background: rgba(255, 255, 255, 0.03); 515 border-radius: 6px; 516 margin-bottom: 6px; 517 font-size: 11px; 518 } 519 520 .backend-welcome .info-label { 521 color: #666; 522 } 523 524 .backend-welcome .info-value { 525 color: #aaa; 526 font-family: 'SF Mono', 'Fira Code', monospace; 527 font-size: 10px; 528 } 529 530 .backend-welcome .info-value.success { 531 color: #0f0; 532 } 533 534 .backend-welcome .info-value.warning { 535 color: #ff9500; 536 } 537 538 .backend-welcome .info-value.error { 539 color: #f55; 540 } 541 542 .backend-welcome .info-value.loading { 543 color: #888; 544 } 545 546 .backend-welcome .info-value.loading::after { 547 content: ''; 548 display: inline-block; 549 width: 4px; 550 height: 4px; 551 margin-left: 6px; 552 background: #888; 553 border-radius: 50%; 554 animation: pulse 1s ease-in-out infinite; 555 } 556 557 @keyframes pulse { 558 0%, 100% { opacity: 0.3; transform: scale(0.8); } 559 50% { opacity: 1; transform: scale(1.2); } 560 } 561 562 .backend-welcome .start-button { 563 background: linear-gradient(180deg, #8844ff 0%, #6622dd 100%); 564 color: #fff; 565 border: none; 566 padding: 12px 32px; 567 border-radius: 8px; 568 font-size: 13px; 569 font-weight: 600; 570 cursor: pointer; 571 transition: all 0.2s ease; 572 box-shadow: 0 4px 16px rgba(136, 68, 255, 0.4); 573 margin-top: 8px; 574 } 575 576 .backend-welcome .start-button:hover { 577 background: linear-gradient(180deg, #9955ff 0%, #7733ee 100%); 578 transform: translateY(-1px); 579 box-shadow: 0 6px 20px rgba(136, 68, 255, 0.5); 580 } 581 582 .backend-welcome .start-button:active { 583 transform: translateY(0); 584 } 585 586 .backend-welcome .start-button:disabled { 587 background: #333; 588 color: #666; 589 cursor: not-allowed; 590 box-shadow: none; 591 } 592 593 .backend-welcome .start-button.secondary { 594 background: linear-gradient(180deg, #333 0%, #222 100%); 595 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); 596 margin-top: 8px; 597 padding: 10px 24px; 598 font-size: 12px; 599 } 600 601 .backend-welcome .start-button.secondary:hover { 602 background: linear-gradient(180deg, #444 0%, #333 100%); 603 } 604 605 .backend-welcome .button-row { 606 display: flex; 607 gap: 12px; 608 flex-wrap: wrap; 609 justify-content: center; 610 } 611 612 .resize-overlay.active { 613 display: block; 614 } 615 616 /* Spinner cursor during swivel animation */ 617 body.swiveling, 618 body.swiveling * { 619 cursor: wait !important; 620 } 621 622 /* Disable pointer events on content during resize */ 623 body.resizing .view, 624 body.resizing webview { 625 pointer-events: none !important; 626 } 627 628 /* PaperWM: full-bleed webview only, no card chrome, no back face */ 629 html.paperwm { background: #000; } 630 html.paperwm body { perspective: none; } 631 html.paperwm .flip-perspective { 632 top: 0; left: 0; right: 0; bottom: 0; 633 perspective: none; 634 } 635 html.paperwm .flip-card { 636 transform-style: flat; 637 transition: none; 638 } 639 html.paperwm .card-face { 640 border: none; 641 border-radius: 0; 642 box-shadow: none; 643 } 644 html.paperwm .card-back, 645 html.paperwm .flip-tab, 646 html.paperwm .mode-tags, 647 html.paperwm .volume-control, 648 html.paperwm .backend-controls, 649 html.paperwm .resize-handle, 650 html.paperwm .resize-overlay { 651 display: none !important; 652 } 653 html.paperwm .card-front { 654 position: absolute; 655 inset: 0; 656 } 657 html.paperwm .card-front webview { 658 width: 100% !important; 659 height: 100% !important; 660 } 661 662 </style> 663</head> 664<body> 665 <!-- Overlay to capture mouse during resize --> 666 <div class="resize-overlay" id="resize-overlay"></div> 667 668 <!-- Resize handles on the card border (split on sides to avoid flip tabs) --> 669 <div class="resize-handle top" data-resize="top"></div> 670 <div class="resize-handle bottom" data-resize="bottom"></div> 671 <div class="resize-handle left-top" data-resize="left"></div> 672 <div class="resize-handle left-bottom" data-resize="left"></div> 673 <div class="resize-handle right-top" data-resize="right"></div> 674 <div class="resize-handle right-bottom" data-resize="right"></div> 675 <div class="resize-handle top-left" data-resize="top-left"></div> 676 <div class="resize-handle top-right" data-resize="top-right"></div> 677 <div class="resize-handle bottom-left" data-resize="bottom-left"></div> 678 <div class="resize-handle bottom-right" data-resize="bottom-right"></div> 679 680 <!-- Perspective container --> 681 <div class="flip-perspective"> 682 <!-- The flipping card with front and back faces --> 683 <div class="flip-card"> 684 <!-- Flip tabs - inside card so they swivel with it --> 685 <div class="flip-tab left" data-flip> 686 <div class="tab-front"></div> 687 <div class="tab-back"></div> 688 </div> 689 <div class="flip-tab right" data-flip> 690 <div class="tab-front"></div> 691 <div class="tab-back"></div> 692 </div> 693 694 <div class="volume-control" aria-label="Master volume"> 695 <input id="volume-slider" type="range" min="0" max="1" step="0.01" value="0.25" /> 696 </div> 697 698 <!-- Shutdown button - visible on backend side --> 699 <div class="backend-controls"> 700 <button id="start-container-btn" class="start-btn"></button> 701 <button id="stop-container-btn" class="stop-btn"></button> 702 </div> 703 704 <!-- Front face: AC Webview --> 705 <div class="card-face card-front"> 706 <webview id="front-webview" src="https://aesthetic.computer/prompt?desktop" allowpopups preload="../webview-preload.js"></webview> 707 </div> 708 709 <!-- Back face: Terminal --> 710 <div class="card-face card-back"> 711 <!-- Welcome screen before terminal starts --> 712 <div class="backend-welcome" id="backend-welcome"> 713 <h1>⚡ AC Pane</h1> 714 <div class="subtitle">Development Environment</div> 715 716 <div class="info-section"> 717 <div class="info-row"> 718 <span class="info-label">📁 Repository</span> 719 <span class="info-value loading" id="info-repo">checking</span> 720 </div> 721 <div class="info-row"> 722 <span class="info-label">👤 Git User</span> 723 <span class="info-value loading" id="info-git-user">checking</span> 724 </div> 725 <div class="info-row"> 726 <span class="info-label">🐳 Docker</span> 727 <span class="info-value loading" id="info-docker">checking</span> 728 </div> 729 <div class="info-row"> 730 <span class="info-label">📦 Container</span> 731 <span class="info-value loading" id="info-container">checking</span> 732 </div> 733 </div> 734 735 <div class="hint-text">Use the START button below to launch the devcontainer</div> 736 </div> 737 738 <div id="terminal-container"></div> 739 </div> 740 </div> 741 </div> <!-- close flip-perspective --> 742 743 <script> 744 const isPaperWM = document.documentElement.classList.contains('paperwm'); 745 const { ipcRenderer } = require('electron'); 746 // Only load terminal deps if we have a back face 747 const Terminal = isPaperWM ? null : require('@xterm/xterm').Terminal; 748 const FitAddon = isPaperWM ? null : require('@xterm/addon-fit').FitAddon; 749 const WebglAddon = isPaperWM ? null : require('@xterm/addon-webgl').WebglAddon; 750 751 const flipCard = document.querySelector('.flip-card'); 752 const webviewEl = document.getElementById('front-webview'); 753 const flipTabs = document.querySelectorAll('.flip-tab'); 754 const volumeSlider = document.getElementById('volume-slider'); 755 let showingTerminal = false; 756 let webviewReady = false; 757 let pendingMasterVolume = null; 758 759 const DEFAULT_MASTER_VOLUME = 0.25; 760 const VOLUME_STORAGE_KEY = 'ac-master-volume'; 761 762 function clampVolume(value) { 763 const numeric = Number(value); 764 if (!Number.isFinite(numeric)) return DEFAULT_MASTER_VOLUME; 765 return Math.min(1, Math.max(0, numeric)); 766 } 767 768 function updateVolumeUI(value) { 769 const clamped = clampVolume(value); 770 volumeSlider.value = clamped.toString(); 771 return clamped; 772 } 773 774 function sendMasterVolume(value) { 775 const clamped = clampVolume(value); 776 console.log('[volume] Sending master volume:', clamped); 777 if (typeof webviewEl?.executeJavaScript !== 'function') { 778 pendingMasterVolume = clamped; 779 return false; 780 } 781 const code = `(() => { 782 const v = ${clamped}; 783 console.log('[AC] setMasterVolume called with:', v); 784 if (window.AC && typeof window.AC.setMasterVolume === 'function') { 785 const result = window.AC.setMasterVolume(v); 786 console.log('[AC] setMasterVolume result:', result); 787 return { success: true, method: 'AC.setMasterVolume', result }; 788 } 789 if (typeof window.acSetMasterVolume === 'function') { 790 const result = window.acSetMasterVolume(v); 791 console.log('[AC] acSetMasterVolume result:', result); 792 return { success: true, method: 'acSetMasterVolume', result }; 793 } 794 console.warn('[AC] No volume setter found. AC:', window.AC); 795 return { success: false, AC: !!window.AC }; 796 })()`; 797 webviewEl.executeJavaScript(code, true) 798 .then(result => console.log('[volume] executeJavaScript result:', result)) 799 .catch(err => console.error('[volume] executeJavaScript error:', err)); 800 pendingMasterVolume = null; 801 return true; 802 } 803 804 function syncMasterVolume() { 805 const clamped = clampVolume(volumeSlider.value); 806 sessionStorage.setItem(VOLUME_STORAGE_KEY, String(clamped)); 807 if (!webviewReady) { 808 pendingMasterVolume = clamped; 809 return; 810 } 811 sendMasterVolume(clamped); 812 } 813 814 // Initialize per-window volume 815 const storedVolume = sessionStorage.getItem(VOLUME_STORAGE_KEY); 816 if (storedVolume !== null) { 817 updateVolumeUI(storedVolume); 818 } else { 819 updateVolumeUI(DEFAULT_MASTER_VOLUME); 820 sessionStorage.setItem(VOLUME_STORAGE_KEY, String(DEFAULT_MASTER_VOLUME)); 821 } 822 syncMasterVolume(); 823 824 825 // Extract piece name from URL 826 function extractPieceName(url) { 827 try { 828 const parsed = new URL(url); 829 const pathname = parsed.pathname; 830 // Remove leading slash and get first segment 831 const piece = pathname.replace(/^\//, '').split('/')[0] || 'prompt'; 832 return piece; 833 } catch (e) { 834 return 'prompt'; 835 } 836 } 837 838 // Track navigation and update tray title 839 webviewEl.addEventListener('did-navigate', (e) => { 840 const piece = extractPieceName(e.url); 841 console.log('[flip] Navigated to piece:', piece); 842 ipcRenderer.invoke('set-current-piece', piece); 843 syncMasterVolume(); 844 }); 845 846 webviewEl.addEventListener('did-navigate-in-page', (e) => { 847 if (e.isMainFrame) { 848 const piece = extractPieceName(e.url); 849 console.log('[flip] In-page navigation to piece:', piece); 850 ipcRenderer.invoke('set-current-piece', piece); 851 syncMasterVolume(); 852 } 853 }); 854 855 webviewEl.addEventListener('dom-ready', () => { 856 console.log('[volume] dom-ready - syncing volume') 857 webviewReady = true; 858 if (pendingMasterVolume !== null) { 859 sendMasterVolume(pendingMasterVolume); 860 } 861 syncMasterVolume(); 862 // Retry after AC has had time to boot 863 setTimeout(() => { 864 console.log('[volume] Retry sync after 2s delay'); 865 syncMasterVolume(); 866 }, 2000); 867 setTimeout(() => { 868 console.log('[volume] Retry sync after 5s delay'); 869 syncMasterVolume(); 870 }, 5000); 871 }); 872 873 // Handle IPC from main process to open devtools 874 ipcRenderer.on('open-devtools', () => { 875 console.log('[flip] Received open-devtools from main'); 876 webviewEl.openDevTools(); 877 }); 878 879 // Handle context menu on webview (right-click) 880 webviewEl.addEventListener('context-menu', (e) => { 881 console.log('[flip] Webview context-menu event', e.params); 882 e.preventDefault(); 883 showContextMenu(e.params.x, e.params.y); 884 }); 885 886 // Handle IPC messages from webview preload (the primary communication channel) 887 webviewEl.addEventListener('ipc-message', (e) => { 888 const channel = e.channel; 889 const args = e.args?.[0] || {}; 890 console.log('[flip] ipc-message from webview:', channel, args); 891 892 if (channel === 'ac-open-window') { 893 const { url, index = 0, total = 1 } = args; 894 console.log('[flip] Opening new window via ipc-message:', url, index, total); 895 ipcRenderer.invoke('ac-open-window', { url, index, total }); 896 } else if (channel === 'ac-close-window') { 897 console.log('[flip] Closing window via ipc-message'); 898 ipcRenderer.invoke('ac-close-window'); 899 } 900 }); 901 902 // Handle window.open() from webview (fallback for older code paths) 903 // Note: Main process also handles this via setWindowOpenHandler 904 webviewEl.addEventListener('new-window', (e) => { 905 console.log('[flip] new-window event received:', e?.url, e); 906 if (e?.preventDefault) e.preventDefault(); 907 const url = e?.url || ''; 908 if (url.startsWith('ac://close')) { 909 console.log('[flip] Handling ac://close - invoking ac-close-window'); 910 ipcRenderer.invoke('ac-close-window'); 911 return; 912 } 913 914 // Check if this is an external URL that should open in the system browser 915 try { 916 const urlObj = new URL(url); 917 const isExternal = !urlObj.hostname.includes('aesthetic.computer') && 918 !urlObj.hostname.includes('localhost') && 919 !urlObj.hostname.includes('127.0.0.1') && 920 !url.startsWith('ac://'); 921 922 if (isExternal) { 923 console.log('[flip] Opening external URL in system browser:', url); 924 ipcRenderer.invoke('open-external-url', url); 925 return; 926 } 927 } catch (err) { 928 console.warn('[flip] Failed to parse URL:', url, err.message); 929 } 930 931 console.log('[flip] Opening new window with url:', url); 932 ipcRenderer.invoke('ac-open-window', { url, index: 0 }); 933 }); 934 935 // Listen for postMessage from webview (legacy fallback) 936 window.addEventListener('message', (e) => { 937 if (e.data?.type === 'ac-open-window') { 938 console.log('[flip] Received ac-open-window postMessage:', e.data); 939 ipcRenderer.invoke('ac-open-window', { 940 url: e.data.url, 941 index: e.data.index || 0, 942 total: e.data.total || 1 943 }); 944 } else if (e.data?.type === 'ac-close-window') { 945 console.log('[flip] Received ac-close-window postMessage'); 946 ipcRenderer.invoke('ac-close-window'); 947 } 948 }); 949 950 // Listen for navigate commands from main process (for new windows) 951 ipcRenderer.on('navigate', (event, url) => { 952 console.log('[flip] Received navigate command:', url); 953 if (url && webviewEl) { 954 webviewEl.src = url; 955 } 956 }); 957 958 // Also try did-create-window (Electron 22+) 959 webviewEl.addEventListener('did-create-window', (e) => { 960 console.log('[flip] did-create-window event:', e); 961 }); 962 963 // And will-navigate for debugging 964 webviewEl.addEventListener('will-navigate', (e) => { 965 console.log('[flip] will-navigate:', e?.url); 966 }); 967 968 // Mark this as the main window and set initial piece 969 ipcRenderer.invoke('set-main-window'); 970 ipcRenderer.invoke('set-current-piece', 'prompt'); 971 972 volumeSlider.addEventListener('input', () => { 973 updateVolumeUI(volumeSlider.value); 974 syncMasterVolume(); 975 }); 976 977 // Container control button handlers 978 const startContainerBtn = document.getElementById('start-container-btn'); 979 const stopContainerBtn = document.getElementById('stop-container-btn'); 980 const welcomeScreen = document.getElementById('backend-welcome'); 981 982 async function updateContainerButtons() { 983 const isRunning = await ipcRenderer.invoke('check-container'); 984 startContainerBtn.disabled = isRunning; 985 stopContainerBtn.disabled = !isRunning; 986 } 987 988 // Show a big centered message in the terminal 989 function showTerminalMessage(message, color = '\x1b[35m') { 990 term.clear(); 991 const rows = term.rows; 992 const cols = term.cols; 993 const midRow = Math.floor(rows / 2); 994 const padding = Math.floor((cols - message.length) / 2); 995 996 // Move to middle and print centered message 997 for (let i = 0; i < midRow - 1; i++) { 998 term.writeln(''); 999 } 1000 term.writeln(color + ' '.repeat(Math.max(0, padding)) + message + '\x1b[0m'); 1001 } 1002 1003 startContainerBtn.addEventListener('click', async () => { 1004 console.log('[flip] Start devcontainer requested'); 1005 startContainerBtn.disabled = true; 1006 startContainerBtn.textContent = '⏳'; 1007 1008 // Clear terminal and show starting message immediately 1009 welcomeScreen.classList.add('hidden'); 1010 fitTerminal(); 1011 showTerminalMessage('▶ STARTING...', '\x1b[35m\x1b[1m'); 1012 1013 try { 1014 await ipcRenderer.invoke('start-flip-devcontainer'); 1015 startContainerBtn.textContent = '▶'; 1016 await updateContainerButtons(); 1017 } catch (err) { 1018 console.error('[flip] Failed to start devcontainer:', err); 1019 showTerminalMessage('✗ START FAILED', '\x1b[31m\x1b[1m'); 1020 startContainerBtn.textContent = '▶'; 1021 startContainerBtn.disabled = false; 1022 } 1023 }); 1024 1025 stopContainerBtn.addEventListener('click', async () => { 1026 console.log('[flip] Stop container requested (aggressive)'); 1027 stopContainerBtn.disabled = true; 1028 stopContainerBtn.textContent = '⏳'; 1029 1030 // Show stopping message immediately 1031 showTerminalMessage('⏹ STOPPING...', '\x1b[35m\x1b[1m'); 1032 1033 try { 1034 await ipcRenderer.invoke('stop-container-aggressive'); 1035 showTerminalMessage('⏹ STOPPED', '\x1b[90m'); 1036 stopContainerBtn.textContent = '⏹'; 1037 await updateContainerButtons(); 1038 } catch (err) { 1039 console.error('[flip] Failed to stop container:', err); 1040 showTerminalMessage('✗ STOP FAILED', '\x1b[31m\x1b[1m'); 1041 stopContainerBtn.textContent = '⏹'; 1042 stopContainerBtn.disabled = false; 1043 } 1044 }); 1045 1046 // Update button states periodically 1047 updateContainerButtons(); 1048 setInterval(updateContainerButtons, 5000); 1049 1050 // Listen for global flip shortcut from main process 1051 ipcRenderer.on('toggle-flip', () => { 1052 toggle(); 1053 }); 1054 1055 // Listen for navigate messages (from new window requests) 1056 ipcRenderer.on('navigate', (event, url) => { 1057 console.log('[flip] Navigate to:', url); 1058 if (url && typeof url === 'string') { 1059 webviewEl.src = url; 1060 } 1061 }); 1062 1063 // ========== Zoom Handling ========== 1064 let frontZoom = 1.0; 1065 let terminalFontSize = 10; 1066 1067 ipcRenderer.on('zoom-in', () => { 1068 if (showingTerminal) { 1069 // Zoom terminal by increasing font size 1070 terminalFontSize = Math.min(terminalFontSize + 2, 32); 1071 term.options.fontSize = terminalFontSize; 1072 fitTerminal(); 1073 console.log('[flip] Terminal font size:', terminalFontSize); 1074 } else { 1075 // Zoom webview 1076 frontZoom = Math.min(frontZoom + 0.1, 3.0); 1077 webviewEl.setZoomFactor(frontZoom); 1078 console.log('[flip] Webview zoom:', frontZoom); 1079 } 1080 }); 1081 1082 ipcRenderer.on('zoom-out', () => { 1083 if (showingTerminal) { 1084 // Zoom terminal by decreasing font size 1085 terminalFontSize = Math.max(terminalFontSize - 2, 6); 1086 term.options.fontSize = terminalFontSize; 1087 fitTerminal(); 1088 console.log('[flip] Terminal font size:', terminalFontSize); 1089 } else { 1090 // Zoom webview 1091 frontZoom = Math.max(frontZoom - 0.1, 0.3); 1092 webviewEl.setZoomFactor(frontZoom); 1093 console.log('[flip] Webview zoom:', frontZoom); 1094 } 1095 }); 1096 1097 ipcRenderer.on('zoom-reset', () => { 1098 if (showingTerminal) { 1099 terminalFontSize = 10; 1100 term.options.fontSize = terminalFontSize; 1101 fitTerminal(); 1102 console.log('[flip] Terminal font size reset to:', terminalFontSize); 1103 } else { 1104 frontZoom = 1.0; 1105 webviewEl.setZoomFactor(frontZoom); 1106 console.log('[flip] Webview zoom reset to:', frontZoom); 1107 } 1108 }); 1109 1110 // ========== Terminal Setup ========== 1111 if (isPaperWM) { 1112 // PaperWM: no terminal, no back face — just the webview 1113 var term = null, fitTerminal = () => {}, fitAddon = null; 1114 } 1115 const terminalContainer = !isPaperWM ? document.getElementById('terminal-container') : null; 1116 1117 if (!isPaperWM) { 1118 // Theme definitions for terminal 1119 const darkTermTheme = { 1120 background: '#0a0a12', 1121 foreground: '#eee', 1122 cursor: '#f0f', 1123 cursorAccent: '#0a0a12', 1124 selectionBackground: 'rgba(255, 0, 255, 0.3)', 1125 black: '#1a1a2e', 1126 red: '#ff5555', 1127 green: '#50fa7b', 1128 yellow: '#f1fa8c', 1129 blue: '#6272a4', 1130 magenta: '#ff79c6', 1131 cyan: '#8be9fd', 1132 white: '#f8f8f2', 1133 }; 1134 1135 const lightTermTheme = { 1136 background: '#f5f0c0', 1137 foreground: '#281e5a', 1138 cursor: '#387adf', 1139 cursorAccent: '#f5f0c0', 1140 selectionBackground: 'rgba(56, 122, 223, 0.3)', 1141 black: '#281e5a', 1142 red: '#cc0000', 1143 green: '#006400', 1144 yellow: '#996600', 1145 blue: '#387adf', 1146 magenta: '#8844ff', 1147 cyan: '#0077aa', 1148 white: '#fcf7c5', 1149 }; 1150 1151 // Detect current color scheme 1152 function isDarkMode() { 1153 return window.matchMedia('(prefers-color-scheme: dark)').matches; 1154 } 1155 1156 const term = new Terminal({ 1157 cursorBlink: true, 1158 cursorStyle: 'block', 1159 fontSize: 10, 1160 fontFamily: "'Menlo', 'DejaVu Sans Mono', 'Consolas', 'Liberation Mono', monospace", 1161 theme: isDarkMode() ? darkTermTheme : lightTermTheme, 1162 allowTransparency: true, 1163 scrollback: 5000, 1164 fastScrollModifier: 'alt', 1165 fastScrollSensitivity: 5, 1166 drawBoldTextInBrightColors: true, 1167 }); 1168 1169 // Listen for color scheme changes 1170 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { 1171 console.log('[flip] Color scheme changed to:', e.matches ? 'dark' : 'light'); 1172 term.options.theme = e.matches ? darkTermTheme : lightTermTheme; 1173 }); 1174 1175 const fitAddon = new FitAddon(); 1176 term.loadAddon(fitAddon); 1177 term.open(terminalContainer); 1178 1179 // Load WebGL2 renderer for better performance 1180 try { 1181 const webglAddon = new WebglAddon(); 1182 webglAddon.onContextLoss(() => { 1183 console.warn('[flip] WebGL context lost, disposing addon'); 1184 webglAddon.dispose(); 1185 }); 1186 term.loadAddon(webglAddon); 1187 console.log('[flip] WebGL2 renderer loaded successfully'); 1188 } catch (e) { 1189 console.warn('[flip] WebGL2 renderer failed to load, using canvas fallback:', e.message); 1190 } 1191 1192 // Fit terminal to container and send initial size to PTY 1193 function fitTerminal() { 1194 fitAddon.fit(); 1195 console.log('[flip] Terminal fit:', term.cols, 'x', term.rows); 1196 ipcRenderer.send('flip-pty-resize', term.cols, term.rows); 1197 } 1198 1199 // Initial fit - wait for layout to stabilize, then fit multiple times 1200 function initialFit() { 1201 // Force container dimensions to be calculated 1202 terminalContainer.style.display = 'block'; 1203 terminalContainer.offsetHeight; // Force reflow 1204 1205 fitTerminal(); 1206 } 1207 1208 // Wait for window to fully load and layout to settle 1209 if (document.readyState === 'complete') { 1210 setTimeout(initialFit, 50); 1211 setTimeout(fitTerminal, 200); 1212 setTimeout(fitTerminal, 500); 1213 } else { 1214 window.addEventListener('load', () => { 1215 setTimeout(initialFit, 50); 1216 setTimeout(fitTerminal, 200); 1217 setTimeout(fitTerminal, 500); 1218 }); 1219 } 1220 1221 // Also fit when the flip card becomes visible (showing backend) 1222 const flipObserver = new MutationObserver(() => { 1223 if (showingTerminal) { 1224 requestAnimationFrame(() => { 1225 fitTerminal(); 1226 }); 1227 } 1228 }); 1229 flipObserver.observe(flipCard, { attributes: true, attributeFilter: ['style'] }); 1230 1231 // Resize handling 1232 const resizeObserver = new ResizeObserver(() => { 1233 fitTerminal(); 1234 }); 1235 resizeObserver.observe(terminalContainer); 1236 1237 // PTY communication 1238 ipcRenderer.on('flip-pty-data', (event, data) => { 1239 term.write(data); 1240 }); 1241 1242 // Respond to size requests from main process 1243 ipcRenderer.on('request-terminal-size', () => { 1244 // Force a fit and send current dimensions 1245 fitAddon.fit(); 1246 ipcRenderer.send('flip-pty-resize', term.cols, term.rows); 1247 }); 1248 1249 term.onData((data) => { 1250 ipcRenderer.send('flip-pty-input', data); 1251 }); 1252 1253 // ========== Welcome Screen Logic ========== 1254 const infoRepo = document.getElementById('info-repo'); 1255 const infoGitUser = document.getElementById('info-git-user'); 1256 const infoDocker = document.getElementById('info-docker'); 1257 const infoContainer = document.getElementById('info-container'); 1258 1259 let systemInfo = { 1260 repoPath: null, 1261 gitUser: null, 1262 dockerAvailable: false, 1263 containerExists: false 1264 }; 1265 1266 async function gatherSystemInfo() { 1267 // Get repo path 1268 try { 1269 const repo = await ipcRenderer.invoke('get-repo-path'); 1270 if (repo) { 1271 systemInfo.repoPath = repo.path; 1272 infoRepo.textContent = repo.name; 1273 infoRepo.classList.remove('loading'); 1274 infoRepo.classList.add('success'); 1275 } else { 1276 infoRepo.textContent = 'Not found'; 1277 infoRepo.classList.remove('loading'); 1278 infoRepo.classList.add('warning'); 1279 } 1280 } catch (e) { 1281 infoRepo.textContent = 'Error'; 1282 infoRepo.classList.remove('loading'); 1283 infoRepo.classList.add('error'); 1284 } 1285 1286 // Get git user 1287 try { 1288 const gitUser = await ipcRenderer.invoke('get-git-user'); 1289 if (gitUser && gitUser.name) { 1290 systemInfo.gitUser = gitUser; 1291 infoGitUser.textContent = gitUser.name; 1292 infoGitUser.classList.remove('loading'); 1293 infoGitUser.classList.add('success'); 1294 } else { 1295 infoGitUser.textContent = 'Not configured'; 1296 infoGitUser.classList.remove('loading'); 1297 infoGitUser.classList.add('warning'); 1298 } 1299 } catch (e) { 1300 infoGitUser.textContent = 'Error'; 1301 infoGitUser.classList.remove('loading'); 1302 infoGitUser.classList.add('error'); 1303 } 1304 1305 // Check Docker 1306 try { 1307 const dockerOk = await ipcRenderer.invoke('check-docker'); 1308 systemInfo.dockerAvailable = dockerOk; 1309 if (dockerOk) { 1310 infoDocker.textContent = 'Running'; 1311 infoDocker.classList.remove('loading'); 1312 infoDocker.classList.add('success'); 1313 } else { 1314 infoDocker.textContent = 'Not running'; 1315 infoDocker.classList.remove('loading'); 1316 infoDocker.classList.add('warning'); 1317 } 1318 } catch (e) { 1319 infoDocker.textContent = 'Error'; 1320 infoDocker.classList.remove('loading'); 1321 infoDocker.classList.add('error'); 1322 } 1323 1324 // Check container exists 1325 try { 1326 const containerOk = await ipcRenderer.invoke('check-container-exists'); 1327 systemInfo.containerExists = containerOk; 1328 if (containerOk) { 1329 infoContainer.textContent = 'Ready'; 1330 infoContainer.classList.remove('loading'); 1331 infoContainer.classList.add('success'); 1332 } else { 1333 infoContainer.textContent = 'Not found'; 1334 infoContainer.classList.remove('loading'); 1335 infoContainer.classList.add('warning'); 1336 } 1337 } catch (e) { 1338 infoContainer.textContent = 'Error'; 1339 infoContainer.classList.remove('loading'); 1340 infoContainer.classList.add('error'); 1341 } 1342 } 1343 1344 // Start gathering info immediately 1345 gatherSystemInfo(); 1346 } // end if (!isPaperWM) 1347 1348 // ========== Toggle Logic ========== 1349 let rotationAngle = 0; // Track cumulative rotation 1350 let midflipTimeout = null; 1351 let swivelingTimeout = null; 1352 1353 function toggle(direction = 'right') { 1354 console.log('[flip] toggle() called, direction:', direction, 'rotation was:', rotationAngle); 1355 1356 // Clear any pending timeouts 1357 if (midflipTimeout) { 1358 clearTimeout(midflipTimeout); 1359 } 1360 if (swivelingTimeout) { 1361 clearTimeout(swivelingTimeout); 1362 } 1363 1364 // Show spinner cursor during swivel 1365 document.body.classList.add('swiveling'); 1366 1367 // Always rotate 180deg in the specified direction (continuous spin) 1368 // Right tab = clockwise (positive), Left tab = counter-clockwise (negative) 1369 if (direction === 'right') { 1370 rotationAngle += 180; 1371 } else { 1372 rotationAngle -= 180; 1373 } 1374 1375 // Apply rotation directly to flip-card 1376 flipCard.style.transform = `rotateY(${rotationAngle}deg)`; 1377 1378 // Determine which side will be showing after flip completes 1379 const normalized = ((rotationAngle % 360) + 360) % 360; 1380 const willShowTerminal = (normalized > 90 && normalized < 270); 1381 1382 // Swap translucency when card is edge-on (90°) 1383 // With cubic-bezier(0.4, 0, 0.2, 1) over 700ms, 90° is reached around 260ms 1384 midflipTimeout = setTimeout(() => { 1385 showingTerminal = willShowTerminal; 1386 document.body.classList.toggle('midflip', showingTerminal); 1387 document.body.classList.toggle('flipped', showingTerminal); 1388 console.log('[flip] Edge-on reached - swapping translucency, showingTerminal:', showingTerminal); 1389 }, 260); 1390 1391 // Remove spinner cursor when swivel completes 1392 swivelingTimeout = setTimeout(() => { 1393 document.body.classList.remove('swiveling'); 1394 }, 700); 1395 1396 console.log('[flip] Will show', willShowTerminal ? 'terminal' : 'webview', 'rotation:', rotationAngle, 'normalized:', normalized); 1397 1398 // After flip completes, fit terminal and focus if showing terminal 1399 if (willShowTerminal && term) { 1400 setTimeout(() => { 1401 fitTerminal(); 1402 term.focus(); 1403 }, 700); 1404 } 1405 } 1406 1407 // Flip tabs - click to flip, native drag to move window (PaperWM/Wayland compatible) 1408 // Inner elements have -webkit-app-region: drag for native window dragging 1409 console.log('[flip] Found flip tabs:', flipTabs.length); 1410 flipTabs.forEach(tab => { 1411 // Use mousedown/mouseup to detect clicks (click event doesn't fire with app-region children) 1412 let mouseDownTime = 0; 1413 let mouseDownPos = { x: 0, y: 0 }; 1414 1415 tab.addEventListener('mousedown', (e) => { 1416 mouseDownTime = Date.now(); 1417 mouseDownPos = { x: e.clientX, y: e.clientY }; 1418 }); 1419 1420 tab.addEventListener('mouseup', (e) => { 1421 const timeDiff = Date.now() - mouseDownTime; 1422 const moveDiff = Math.hypot(e.clientX - mouseDownPos.x, e.clientY - mouseDownPos.y); 1423 1424 // If mouse was down for less than 300ms and didn't move much, treat as click 1425 if (timeDiff < 300 && moveDiff < 10) { 1426 e.preventDefault(); 1427 e.stopPropagation(); 1428 1429 // Base direction on SCREEN position, not tab class 1430 const windowCenter = window.innerWidth / 2; 1431 const direction = (e.clientX > windowCenter) ? 'right' : 'left'; 1432 1433 console.log('[flip] Flip tab clicked at x:', e.clientX, 'center:', windowCenter, 'direction:', direction); 1434 toggle(direction); 1435 } 1436 }); 1437 }); 1438 1439 // Keyboard shortcuts 1440 document.addEventListener('keydown', (e) => { 1441 // Tab to toggle flip 1442 if (e.key === 'Tab') { 1443 console.log('[flip] Tab key pressed'); 1444 e.preventDefault(); 1445 toggle(); 1446 } 1447 // Cmd+Shift+I or Ctrl+Shift+I to open webview devtools (check both cases) 1448 if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 'i' || e.key === 'I')) { 1449 e.preventDefault(); 1450 console.log('[flip] Opening webview DevTools via keyboard'); 1451 if (webviewEl.isDevToolsOpened()) { 1452 webviewEl.closeDevTools(); 1453 } else { 1454 webviewEl.openDevTools(); 1455 } 1456 } 1457 // F12 also opens devtools (Windows standard) 1458 if (e.key === 'F12') { 1459 e.preventDefault(); 1460 console.log('[flip] Opening webview DevTools via F12'); 1461 if (webviewEl.isDevToolsOpened()) { 1462 webviewEl.closeDevTools(); 1463 } else { 1464 webviewEl.openDevTools(); 1465 } 1466 } 1467 }); 1468 1469 // Context menu for DevTools 1470 document.addEventListener('contextmenu', (e) => { 1471 // Show custom context menu 1472 e.preventDefault(); 1473 showContextMenu(e.clientX, e.clientY); 1474 }); 1475 1476 // Custom context menu 1477 function showContextMenu(x, y) { 1478 // Remove any existing menu 1479 const existing = document.getElementById('custom-context-menu'); 1480 if (existing) existing.remove(); 1481 1482 const menu = document.createElement('div'); 1483 menu.id = 'custom-context-menu'; 1484 menu.style.cssText = ` 1485 position: fixed; 1486 left: ${x}px; 1487 top: ${y}px; 1488 background: rgba(30, 30, 40, 0.95); 1489 border: 1px solid var(--ac-border); 1490 border-radius: 6px; 1491 padding: 4px 0; 1492 min-width: 160px; 1493 box-shadow: 0 4px 16px rgba(0,0,0,0.4); 1494 z-index: 10000; 1495 font-size: 12px; 1496 backdrop-filter: blur(8px); 1497 `; 1498 1499 const menuItems = [ 1500 { label: '🔧 Open DevTools', action: () => webviewEl.openDevTools() }, 1501 { label: '🔄 Reload Page', action: () => webviewEl.reload() }, 1502 { label: '📋 Copy URL', action: () => navigator.clipboard.writeText(webviewEl.src) }, 1503 { type: 'separator' }, 1504 { label: '🔊 Reset Volume', action: () => { updateVolumeUI(DEFAULT_MASTER_VOLUME); syncMasterVolume(); } }, 1505 ]; 1506 1507 menuItems.forEach(item => { 1508 if (item.type === 'separator') { 1509 const sep = document.createElement('div'); 1510 sep.style.cssText = 'height: 1px; background: var(--ac-border); margin: 4px 8px;'; 1511 menu.appendChild(sep); 1512 } else { 1513 const menuItem = document.createElement('div'); 1514 menuItem.textContent = item.label; 1515 menuItem.style.cssText = ` 1516 padding: 6px 12px; 1517 cursor: pointer; 1518 color: var(--ac-text); 1519 `; 1520 menuItem.addEventListener('mouseenter', () => { 1521 menuItem.style.background = 'var(--ac-border-hover)'; 1522 }); 1523 menuItem.addEventListener('mouseleave', () => { 1524 menuItem.style.background = 'transparent'; 1525 }); 1526 menuItem.addEventListener('click', () => { 1527 item.action(); 1528 menu.remove(); 1529 }); 1530 menu.appendChild(menuItem); 1531 } 1532 }); 1533 1534 document.body.appendChild(menu); 1535 1536 // Close menu on click outside 1537 const closeMenu = (e) => { 1538 if (!menu.contains(e.target)) { 1539 menu.remove(); 1540 document.removeEventListener('click', closeMenu); 1541 } 1542 }; 1543 setTimeout(() => document.addEventListener('click', closeMenu), 0); 1544 } 1545 1546 // Window resize 1547 window.addEventListener('resize', () => { 1548 fitAddon.fit(); 1549 ipcRenderer.send('flip-pty-resize', term.cols, term.rows); 1550 }); 1551 1552 // ========== Custom Resize Handles ========== 1553 const resizeOverlay = document.getElementById('resize-overlay'); 1554 1555 document.querySelectorAll('[data-resize]').forEach(handle => { 1556 handle.addEventListener('mousedown', (e) => { 1557 e.preventDefault(); 1558 e.stopPropagation(); 1559 const direction = handle.dataset.resize; 1560 1561 // Get cursor style for this direction 1562 const cursorStyle = window.getComputedStyle(handle).cursor; 1563 1564 // Show overlay and set body class to disable pointer events on content 1565 resizeOverlay.style.cursor = cursorStyle; 1566 resizeOverlay.classList.add('active'); 1567 document.body.classList.add('resizing'); 1568 1569 const startX = e.screenX; 1570 const startY = e.screenY; 1571 const startBounds = { 1572 x: window.screenX, 1573 y: window.screenY, 1574 width: window.outerWidth, 1575 height: window.outerHeight 1576 }; 1577 1578 const onMouseMove = (e) => { 1579 const dx = e.screenX - startX; 1580 const dy = e.screenY - startY; 1581 1582 let newX = startBounds.x; 1583 let newY = startBounds.y; 1584 let newWidth = startBounds.width; 1585 let newHeight = startBounds.height; 1586 1587 if (direction.includes('left')) { 1588 newX = startBounds.x + dx; 1589 newWidth = startBounds.width - dx; 1590 } 1591 if (direction.includes('right')) { 1592 newWidth = startBounds.width + dx; 1593 } 1594 if (direction.includes('top')) { 1595 newY = startBounds.y + dy; 1596 newHeight = startBounds.height - dy; 1597 } 1598 if (direction.includes('bottom')) { 1599 newHeight = startBounds.height + dy; 1600 } 1601 1602 // Minimum size 1603 if (newWidth < 400) { newWidth = 400; newX = startBounds.x + startBounds.width - 400; } 1604 if (newHeight < 300) { newHeight = 300; newY = startBounds.y + startBounds.height - 300; } 1605 1606 ipcRenderer.send('resize-window', { x: newX, y: newY, width: newWidth, height: newHeight }); 1607 }; 1608 1609 const onMouseUp = () => { 1610 // Hide overlay and restore pointer events 1611 resizeOverlay.classList.remove('active'); 1612 document.body.classList.remove('resizing'); 1613 1614 document.removeEventListener('mousemove', onMouseMove); 1615 document.removeEventListener('mouseup', onMouseUp); 1616 }; 1617 1618 document.addEventListener('mousemove', onMouseMove); 1619 document.addEventListener('mouseup', onMouseUp); 1620 }); 1621 }); 1622 1623 // ========== Alt + Scroll to Drag Window ========== 1624 let scrollDragActive = false; 1625 let scrollDragStart = { x: 0, y: 0 }; 1626 1627 document.addEventListener('wheel', (e) => { 1628 // Alt + scroll (two-finger drag with alt) moves the window 1629 if (e.altKey) { 1630 e.preventDefault(); 1631 1632 // Use deltaX and deltaY to move window 1633 const currentX = window.screenX; 1634 const currentY = window.screenY; 1635 1636 // Invert and scale the delta for natural dragging feel 1637 const newX = currentX - e.deltaX; 1638 const newY = currentY - e.deltaY; 1639 1640 ipcRenderer.send('move-window', { x: Math.round(newX), y: Math.round(newY) }); 1641 } 1642 }, { passive: false }); 1643 </script> 1644</body> 1645</html>