Spaces:
Build error
Build error
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>23DFactory - Visualizador Móvil</title> | |
| <!-- Import Google's model-viewer component --> | |
| <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script> | |
| <!-- Cropper.js for Interactive Image Cropping --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script> | |
| <!-- Supabase JS Client --> | |
| <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script> | |
| <style> | |
| body, html { | |
| margin: 0; | |
| padding: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: #121214; | |
| color: #ffffff; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| overflow-x: hidden; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| header { | |
| padding: 10px 16px; | |
| background-color: rgba(18, 18, 20, 0.95); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.08); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| position: relative; | |
| z-index: 1000; | |
| min-height: 48px; | |
| box-sizing: border-box; | |
| } | |
| .logo { | |
| font-weight: 800; | |
| font-size: 1.1rem; | |
| letter-spacing: -0.5px; | |
| background: linear-gradient(135deg, #a78bfa, #c084fc); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .btn-close { | |
| background: none; | |
| border: none; | |
| color: #a1a1aa; | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| text-decoration: none; | |
| } | |
| /* Container for viewer */ | |
| #viewer-container { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| display: none; | |
| } | |
| model-viewer { | |
| width: 100%; | |
| height: 100%; | |
| background-color: #121214; | |
| } | |
| /* Custom AR button styling */ | |
| #ar-button { | |
| background-color: #7c3aed; | |
| color: white; | |
| border-radius: 8px; | |
| border: none; | |
| position: absolute; | |
| bottom: 24px; | |
| right: 24px; | |
| padding: 14px 20px; | |
| font-size: 0.95rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| z-index: 99; | |
| } | |
| #ar-prompt { | |
| position: absolute; | |
| left: 50%; | |
| bottom: 100px; | |
| transform: translateX(-50%); | |
| background-color: rgba(18, 18, 20, 0.85); | |
| color: #ffffff; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| display: none; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| /* Loading state styling */ | |
| #loading-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #121214; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 5; | |
| transition: opacity 0.5s; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 3px solid rgba(255, 255, 255, 0.05); | |
| border-top: 3px solid #7c3aed; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-bottom: 16px; | |
| } | |
| .spinner-small { | |
| width: 14px; | |
| height: 14px; | |
| border: 2px solid rgba(255, 255, 255, 0.1); | |
| border-top: 2px solid #7c3aed; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| display: inline-block; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Gallery Selector Page */ | |
| #gallery-container { | |
| flex: 1; | |
| padding: 16px 20px 20px 20px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| box-sizing: border-box; | |
| } | |
| #creator-container { | |
| flex: 1; | |
| padding: 16px 20px 20px 20px ; | |
| overflow-y: auto; | |
| box-sizing: border-box; | |
| } | |
| .gallery-title { | |
| font-size: 1.35rem; | |
| font-weight: 700; | |
| margin-bottom: 4px; | |
| background: linear-gradient(135deg, #ffffff, #a1a1aa); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .gallery-desc { | |
| font-size: 0.85rem; | |
| color: #a1a1aa; | |
| margin-bottom: 16px; | |
| line-height: 1.4; | |
| } | |
| .gallery-grid { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 16px; | |
| } | |
| .model-card { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| border-radius: 12px; | |
| padding: 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| transition: border-color 0.2s; | |
| } | |
| .model-card:active { | |
| border-color: #7c3aed; | |
| background: rgba(124, 58, 237, 0.05); | |
| } | |
| .model-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .model-icon { | |
| background: rgba(124, 58, 237, 0.1); | |
| color: #c084fc; | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.3rem; | |
| } | |
| .model-name { | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| color: #ffffff; | |
| margin-bottom: 4px; | |
| } | |
| .model-date { | |
| font-size: 0.75rem; | |
| color: #a1a1aa; | |
| } | |
| .btn-view { | |
| background: #7c3aed; | |
| color: #ffffff; | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| } | |
| .btn-back { | |
| position: absolute; | |
| top: 12px; | |
| left: 16px; | |
| background: rgba(255, 255, 255, 0.08); | |
| border: none; | |
| color: #ffffff; | |
| padding: 6px 12px; | |
| border-radius: 6px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| z-index: 99; | |
| cursor: pointer; | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header id="app-header"> | |
| <span class="logo">23DFactory</span> | |
| <div style="display: flex; gap: 8px; align-items: center;"> | |
| <!-- Mobile Background Task Indicator Banner --> | |
| <div id="mobBgTaskBanner" style="display: none; align-items: center; gap: 6px; background: rgba(124, 58, 237, 0.2); border: 1px solid rgba(167, 139, 250, 0.5); backdrop-filter: blur(8px); padding: 4px 10px; border-radius: 16px; font-size: 0.75rem; color: #ffffff; box-shadow: 0 0 10px rgba(124, 58, 237, 0.3);"> | |
| <div class="spinner-small" style="width: 11px; height: 11px; border-width: 2px; margin: 0;"></div> | |
| <span id="mobBgTaskText" style="font-weight: 600;">Generando... 0%</span> | |
| </div> | |
| <div id="mobUserCredits" style="display: none; position: relative;"> | |
| <button id="btnMobUserMenu" style="background: linear-gradient(135deg, rgba(124, 58, 237, 0.4), rgba(167, 139, 250, 0.2)); border: 1.5px solid rgba(167, 139, 250, 0.6); color: #ffffff; padding: 0; border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 0 10px rgba(124, 58, 237, 0.3); overflow: hidden; position: relative;" title="Menú de usuario"> | |
| <img id="mobUserAvatarImg" src="" alt="Avatar" style="width: 100%; height: 100%; object-fit: cover; display: none;"> | |
| <span id="mobUserAvatarInitial" style="font-size: 0.9rem; font-weight: 800; color: #ffffff; text-transform: uppercase;">P</span> | |
| </button> | |
| <div id="mobUserDropdown" style="display: none; position: absolute; top: calc(100% + 8px); right: 0; background: #161622; border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 12px; padding: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.6); z-index: 1000; min-width: 210px; width: max-content;"> | |
| <div style="padding: 6px 10px; margin-bottom: 4px; font-size: 0.85rem; font-weight: 700; color: #ffffff; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid rgba(255,255,255,0.08); white-space: nowrap;"> | |
| 👤 <span id="mobUsername">-</span> | |
| </div> | |
| <div style="padding: 6px 10px; margin-bottom: 6px; font-size: 0.8rem; font-weight: 700; color: #ffeb3b; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid rgba(255,255,255,0.08); white-space: nowrap;"> | |
| 🪙 <span id="mobCredits">0</span> Créditos | |
| </div> | |
| <button id="btnMobProfile" style="width: 100%; text-align: left; background: transparent; border: none; color: #ffffff; padding: 8px 10px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 6px; white-space: nowrap;"> | |
| 👤 <span>Mi perfil</span> | |
| </button> | |
| <button id="btnMobPrivacy" style="width: 100%; text-align: left; background: transparent; border: none; color: #ffffff; padding: 8px 10px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 6px; white-space: nowrap;"> | |
| 🛡️ <span>Privacidad y Seguridad</span> | |
| </button> | |
| <div style="height: 1px; background: rgba(255,255,255,0.08); margin: 4px 0;"></div> | |
| <button id="btnMobLogout" style="width: 100%; text-align: left; background: transparent; border: none; color: #ef4444; padding: 8px 10px; border-radius: 8px; font-size: 0.8rem; font-weight: 700; cursor: pointer; display: flex; align-items: center; gap: 6px; white-space: nowrap;"> | |
| 🚪 <span>Salir</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Mobile Navigation Tab Bar --> | |
| <nav id="mobNavigation" style="display: none; background: rgba(20, 20, 28, 0.95); border-bottom: 1px solid rgba(255, 255, 255, 0.08); padding: 8px 12px; gap: 8px; justify-content: center; position: sticky; top: 0; z-index: 900; backdrop-filter: blur(10px);"> | |
| <button id="navTab3D" class="mob-nav-btn" style="flex: 1; background: transparent; border: 1px solid rgba(255, 255, 255, 0.1); color: #a1a1aa; padding: 8px 6px; border-radius: 10px; font-weight: 700; font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.2s;"> | |
| 📦 Generar 3D | |
| </button> | |
| <button id="navTab2D" class="mob-nav-btn" style="flex: 1; background: transparent; border: 1px solid rgba(255, 255, 255, 0.1); color: #a1a1aa; padding: 8px 6px; border-radius: 10px; font-weight: 700; font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.2s;"> | |
| 🖼️ Generar 2D | |
| </button> | |
| <button id="navTabGallery" class="mob-nav-btn active" style="flex: 1; background: #7c3aed; border: 1px solid #7c3aed; color: #ffffff; padding: 8px 6px; border-radius: 10px; font-weight: 700; font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.2s; box-shadow: 0 0 10px rgba(124, 58, 237, 0.4);"> | |
| 📖 Biblioteca | |
| </button> | |
| </nav> | |
| <button id="btnBackToGallery" class="btn-back">⬅ Volver</button> | |
| <!-- Mobile Login / Register Modal/Container (shown when unauthenticated) --> | |
| <div id="mobLoginContainer" style="display: none; flex-direction: column; max-width: 400px; margin: 20px auto; width: 100%; box-sizing: border-box; padding: 20px;"> | |
| <div style="background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px; padding: 24px; text-align: left; box-shadow: 0 10px 30px rgba(0,0,0,0.5);"> | |
| <div style="display: flex; background: rgba(0, 0, 0, 0.3); border-radius: 10px; padding: 3px; border: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 20px;"> | |
| <button type="button" id="tabMobLogin" style="flex: 1; background: #7c3aed; border: none; color: #fff; padding: 10px; border-radius: 8px; font-size: 0.85rem; font-weight: 700; cursor: pointer; transition: all 0.2s;">Iniciar Sesión</button> | |
| <button type="button" id="tabMobRegister" style="flex: 1; background: transparent; border: none; color: #a1a1aa; padding: 10px; border-radius: 8px; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.2s;">Registrarse</button> | |
| </div> | |
| <!-- Login Form --> | |
| <form id="mobLoginForm" style="display:flex; flex-direction:column; gap:14px;"> | |
| <h3 style="margin-top:0; color:#fff; font-size:1.2rem; margin-bottom:4px; font-weight:700; font-family:'Outfit', sans-serif;">¡Bienvenido de nuevo!</h3> | |
| <p style="font-size:0.8rem; color:#a1a1aa; margin-bottom:12px; line-height:1.4;">Ingresa tus credenciales para acceder a tu biblioteca y crear modelos 3D.</p> | |
| <div> | |
| <label style="font-size:0.8rem; color:#a1a1aa; display:block; margin-bottom:6px; font-weight:600;">Usuario</label> | |
| <input type="text" id="mobUser" required autocomplete="username" style="width:100%; box-sizing:border-box; padding:12px; background:#1e1e24; border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; outline:none; font-family:inherit; font-size:0.9rem;"> | |
| </div> | |
| <div> | |
| <label style="font-size:0.8rem; color:#a1a1aa; display:block; margin-bottom:6px; font-weight:600;">Contraseña</label> | |
| <input type="password" id="mobPass" required autocomplete="current-password" style="width:100%; box-sizing:border-box; padding:12px; background:#1e1e24; border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; outline:none; font-family:inherit; font-size:0.9rem;"> | |
| </div> | |
| <div id="mobError" style="color:#ff5252; font-size:0.8rem; display:none; background:rgba(255, 82, 82, 0.1); border:1px solid rgba(255, 82, 82, 0.2); padding:10px; border-radius:8px;"></div> | |
| <button type="submit" id="btnMobLogin" style="background:#7c3aed; color:#fff; border:none; padding:14px; border-radius:10px; font-weight:700; font-size:0.95rem; cursor:pointer; margin-top:6px; box-shadow:0 4px 12px rgba(124,58,237,0.3);"> | |
| Iniciar Sesión | |
| </button> | |
| </form> | |
| <!-- Register Form --> | |
| <form id="mobRegisterForm" style="display:none; flex-direction:column; gap:14px;"> | |
| <h3 style="margin-top:0; color:#fff; font-size:1.2rem; margin-bottom:4px; font-weight:700; font-family:'Outfit', sans-serif;">Crear Nueva Cuenta</h3> | |
| <p style="font-size:0.8rem; color:#a1a1aa; margin-bottom:12px; line-height:1.4;">Regístrate gratis para comenzar a generar imágenes y modelos 3D.</p> | |
| <div> | |
| <label style="font-size:0.8rem; color:#a1a1aa; display:block; margin-bottom:6px; font-weight:600;">Usuario</label> | |
| <input type="text" id="mobRegUser" required autocomplete="username" placeholder="Crea tu usuario" style="width:100%; box-sizing:border-box; padding:12px; background:#1e1e24; border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; outline:none; font-family:inherit; font-size:0.9rem;"> | |
| </div> | |
| <div> | |
| <label style="font-size:0.8rem; color:#a1a1aa; display:block; margin-bottom:6px; font-weight:600;">Contraseña</label> | |
| <input type="password" id="mobRegPass" required autocomplete="new-password" placeholder="Mínimo 4 caracteres" style="width:100%; box-sizing:border-box; padding:12px; background:#1e1e24; border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; outline:none; font-family:inherit; font-size:0.9rem;"> | |
| </div> | |
| <div> | |
| <label style="font-size:0.8rem; color:#a1a1aa; display:block; margin-bottom:6px; font-weight:600;">Confirmar Contraseña</label> | |
| <input type="password" id="mobRegPassConfirm" required autocomplete="new-password" placeholder="Repite tu contraseña" style="width:100%; box-sizing:border-box; padding:12px; background:#1e1e24; border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; outline:none; font-family:inherit; font-size:0.9rem;"> | |
| </div> | |
| <div id="mobRegError" style="color:#ff5252; font-size:0.8rem; display:none; background:rgba(255, 82, 82, 0.1); border:1px solid rgba(255, 82, 82, 0.2); padding:10px; border-radius:8px;"></div> | |
| <button type="submit" id="btnMobRegister" style="background:#7c3aed; color:#fff; border:none; padding:14px; border-radius:10px; font-weight:700; font-size:0.95rem; cursor:pointer; margin-top:6px; box-shadow:0 4px 12px rgba(124,58,237,0.3);"> | |
| Registrarse | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- 1. Gallery selector (shown if no model query param) --> | |
| <div id="gallery-container" style="display: none;"> | |
| <div id="mobGalleryHeader"> | |
| <h2 class="gallery-title" style="margin-top: 0;">Tu Biblioteca 3D y 2D</h2> | |
| <p class="gallery-desc">Tus imágenes generadas y modelos 3D listos para visualizar o proyectar en AR.</p> | |
| </div> | |
| <!-- Library Filter Tabs --> | |
| <div id="mobFilterTabs" style="display: flex; gap: 8px; margin-bottom: 16px; background: rgba(255,255,255,0.03); padding: 4px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.06);"> | |
| <button id="mobFilterAll" class="mob-filter-btn active" style="flex: 1; background: #7c3aed; border: none; color: #fff; padding: 8px; border-radius: 8px; font-size: 0.8rem; font-weight: 700; cursor: pointer;">Todos</button> | |
| <button id="mobFilter2D" class="mob-filter-btn" style="flex: 1; background: transparent; border: none; color: #a1a1aa; padding: 8px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer;">Imágenes 2D</button> | |
| <button id="mobFilter3D" class="mob-filter-btn" style="flex: 1; background: transparent; border: none; color: #a1a1aa; padding: 8px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; cursor: pointer;">Modelos 3D</button> | |
| </div> | |
| <div class="gallery-grid" id="gallery-grid"> | |
| <div style="text-align: center; padding: 40px; color: #a1a1aa; font-size: 0.9rem;"> | |
| <div class="spinner" style="margin: 0 auto 16px auto;"></div> | |
| Cargando tu galería... | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 3. Mobile Creator Container (hidden by default) --> | |
| <div id="creator-container" style="display: none; flex-direction: column; max-width: 600px; margin: 0 auto; width: 100%;"> | |
| <div style="margin-bottom: 16px;"> | |
| <h2 id="creatorSectionTitle" style="font-family: 'Outfit', sans-serif; font-size: 1.4rem; font-weight: 700; margin: 0;">Crear Modelo 3D</h2> | |
| </div> | |
| <!-- Creator Tabs --> | |
| <div id="creatorTabHeader" style="display: flex; background: rgba(255,255,255,0.05); border-radius: 10px; padding: 4px; margin-bottom: 20px;"> | |
| <button id="tabUpload" style="flex: 1; background: #27272a; border: none; color: #fff; padding: 10px; border-radius: 8px; font-size: 0.85rem; font-weight: 600; cursor: pointer;">Subir Foto</button> | |
| <button id="tabGallerySelect" style="flex: 1; background: transparent; border: none; color: #a1a1aa; padding: 10px; border-radius: 8px; font-size: 0.85rem; font-weight: 600; cursor: pointer;">Elegir de Galería 2D</button> | |
| </div> | |
| <!-- Creator Contents --> | |
| <div id="creator-contents" style="background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px;"> | |
| <!-- Upload Tab Panel --> | |
| <div id="panelUpload" style="display: flex; flex-direction: column; gap: 16px; text-align: center;"> | |
| <div id="dropzone" style="border: 2px dashed rgba(124,58,237,0.3); border-radius: 12px; padding: 35px 20px; background: rgba(124,58,237,0.02); cursor: pointer; text-align: center;"> | |
| <div style="font-size: 2.8rem; margin-bottom: 12px;">🖼️</div> | |
| <div style="font-size: 0.95rem; font-weight: 600; color: #fff; margin-bottom: 4px;">Sube una foto o imagen desde tu celular</div> | |
| <div style="font-size: 0.8rem; color: #a1a1aa;">Formato JPG o PNG con fondo claro/limpio</div> | |
| <input type="file" id="fileInputMobile" accept="image/*" style="display: none;"> | |
| </div> | |
| <div id="imagePreviewContainer" style="display: none; position: relative;"> | |
| <img id="imagePreviewMobile" style="max-width: 100%; max-height: 250px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); margin: 0 auto; display: block;"> | |
| <button id="btnRemoveImageMobile" style="position: absolute; top: 8px; right: 8px; background: rgba(239,68,68,0.9); color: white; border: none; border-radius: 50%; width: 28px; height: 28px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 0.8rem;">✕</button> | |
| <button type="button" id="btnOpenCropMobile" style="margin-top: 10px; width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 0.85rem; padding: 10px; background: rgba(124, 58, 237, 0.2); border: 1px solid rgba(124, 58, 237, 0.5); color: #a78bfa; border-radius: 8px; cursor: pointer; font-weight: 600;"> | |
| ✂️ Enmarcar / Recortar Sujeto | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Gallery Selector Tab Panel --> | |
| <div id="panelGallerySelect" style="display: none; flex-direction: column; gap: 12px; text-align: left;"> | |
| <div style="font-size: 0.85rem; color: #a1a1aa; margin-bottom: 4px;">Selecciona una de tus imágenes 2D almacenadas:</div> | |
| <div id="mobGalleryImageGrid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; max-height: 280px; overflow-y: auto; padding-right: 4px;"> | |
| <div style="color: #a1a1aa; font-size: 0.8rem; grid-column: span 2; text-align: center; padding: 20px;">Cargando imágenes...</div> | |
| </div> | |
| </div> | |
| <!-- Prompt Tab Panel (Generar 2D) --> | |
| <div id="panelPrompt" style="display: none; flex-direction: column; gap: 14px; text-align: left;"> | |
| <div> | |
| <label style="font-size:0.75rem; color:#a1a1aa; display:block; margin-bottom:6px; font-weight:600;">Describe tu idea (Inglés preferido)</label> | |
| <textarea id="promptInputMobile" rows="3" placeholder="Ej. A cute small red dragon, cartoon style, highly detailed, white background..." style="width:100%; box-sizing:border-box; padding:12px; background:#1e1e24; border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; outline:none; font-family:inherit; font-size:0.9rem; resize: none;"></textarea> | |
| </div> | |
| <button type="button" id="btnGen2DMobile" style="background:#7c3aed; color:#fff; border:none; padding:12px; border-radius:8px; font-weight:700; font-size:0.9rem; cursor:pointer; border-radius:10px;"> | |
| Generar Imagen 2D (Cuesta 1 crédito) | |
| </button> | |
| <div id="aiImageContainer" style="display: none; flex-direction: column; gap: 10px; align-items: center; margin-top: 10px;"> | |
| <img id="aiImageMobile" style="max-width: 100%; max-height: 250px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); margin: 0 auto; display: block;"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Button --> | |
| <div style="margin-top: 20px;"> | |
| <button id="btnCreate3DMobile" style="width: 100%; background: #10b981; color: #fff; border: none; padding: 14px; border-radius: 10px; font-weight: 700; font-size: 1rem; cursor: pointer; display: none; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(10,185,129,0.25);"> | |
| 📦 Crear Modelo 3D (Cuesta 5 créditos) | |
| </button> | |
| </div> | |
| </div> | |
| <!-- 2. Interactive model viewer (shown when active) --> | |
| <div id="viewer-container" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 9999; background: #121214;"> | |
| <!-- Viewer Header Control Overlay --> | |
| <div id="viewerHeaderBar" style="position: absolute; top: 0; left: 0; right: 0; z-index: 10000; display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: rgba(18, 18, 20, 0.9); border-bottom: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);"> | |
| <span class="logo" style="font-size: 1.1rem; user-select: none;">23DFactory</span> | |
| <div style="display: flex; gap: 8px; align-items: center;"> | |
| <button id="btnDownloadViewerFBX" style="background: linear-gradient(135deg, #10b981, #059669); border: none; color: #ffffff; padding: 8px 14px; border-radius: 20px; font-size: 0.8rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); display: flex; align-items: center; justify-content: center; z-index: 10001;"> | |
| Descargar FBX | |
| </button> | |
| <button id="btnBackToGallery" onclick="exitViewerToGallery()" style="background: linear-gradient(135deg, #ef4444, #dc2626); border: none; color: #ffffff; padding: 8px 14px; border-radius: 20px; font-size: 0.8rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); display: flex; align-items: center; justify-content: center; z-index: 10001;"> | |
| Volver | |
| </button> | |
| </div> | |
| </div> | |
| <div id="loading-overlay" style="z-index: 9999;"> | |
| <div class="spinner"></div> | |
| <div id="loading-text" style="font-size: 0.9rem; color: #a1a1aa;">Cargando archivo 3D...</div> | |
| </div> | |
| <model-viewer | |
| id="viewer" | |
| src="" | |
| camera-controls | |
| touch-action="pan-y" | |
| shadow-intensity="1" | |
| auto-rotate | |
| interaction-prompt="auto" | |
| style="width: 100%; height: 100%; padding-top: 50px; box-sizing: border-box;"> | |
| </model-viewer> | |
| </div> | |
| <!-- 2. Interactive 2D Image viewer (shown when active) --> | |
| <div id="image-viewer-container" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 9999; background: #121214; flex-direction: column; justify-content: space-between;"> | |
| <!-- Image Viewer Header Control Overlay --> | |
| <div id="imageViewerHeaderBar" style="position: absolute; top: 0; left: 0; right: 0; z-index: 10000; display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: rgba(18, 18, 20, 0.9); border-bottom: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);"> | |
| <span class="logo" style="font-size: 1.1rem; user-select: none;">23DFactory</span> | |
| <div style="display: flex; gap: 8px; align-items: center;"> | |
| <button id="btnCreate3DFrom2DViewer" style="background: linear-gradient(135deg, #10b981, #059669); border: none; color: #ffffff; padding: 8px 14px; border-radius: 20px; font-size: 0.8rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); display: flex; align-items: center; justify-content: center; z-index: 10001;"> | |
| Crear Modelo 3D | |
| </button> | |
| <button id="btnBackFrom2DViewer" onclick="close2DImageViewer()" style="background: linear-gradient(135deg, #ef4444, #dc2626); border: none; color: #ffffff; padding: 8px 14px; border-radius: 20px; font-size: 0.8rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); display: flex; align-items: center; justify-content: center; z-index: 10001;"> | |
| Volver | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Centered 2D Image View --> | |
| <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; padding: 70px 16px 20px 16px; box-sizing: border-box;"> | |
| <img id="full2DImageView" src="" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1);"> | |
| </div> | |
| </div> | |
| <!-- Interactive Crop Modal Overlay --> | |
| <div id="cropModalOverlay" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: rgba(10, 10, 15, 0.95); backdrop-filter: blur(12px); flex-direction: column; align-items: center; justify-content: space-between; padding: 16px; box-sizing: border-box;"> | |
| <!-- Modal Header --> | |
| <div style="width: 100%; max-width: 600px; display: flex; justify-content: space-between; align-items: center; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.1);"> | |
| <div style="font-weight: 700; font-size: 1rem; color: #fff; display: flex; align-items: center; gap: 8px;"> | |
| ✂️ Enmarcar / Recortar Sujeto | |
| </div> | |
| <button id="btnCancelCrop" style="background: rgba(255,255,255,0.1); border: none; color: #fff; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 1rem; display: flex; align-items: center; justify-content: center;">✕</button> | |
| </div> | |
| <!-- Crop Canvas Viewport Container --> | |
| <div style="width: 100%; max-width: 600px; height: calc(100vh - 180px); max-height: 480px; display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; margin: 12px 0; background: #000; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);"> | |
| <img id="cropImageTarget" src="" style="max-width: 100%; max-height: 100%;"> | |
| </div> | |
| <!-- Modal Controls Footer --> | |
| <div style="width: 100%; max-width: 600px; display: flex; flex-direction: column; gap: 10px;"> | |
| <div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;"> | |
| <button type="button" id="btnCropAspectFree" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Libre</button> | |
| <button type="button" id="btnCropAspectSquare" style="background: rgba(124,58,237,0.3); border: 1px solid #7c3aed; color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">1:1 Cuadrado (Recomendado)</button> | |
| <button type="button" id="btnCropReset" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Reiniciar</button> | |
| </div> | |
| <button type="button" id="btnApplyCrop" style="width: 100%; background: linear-gradient(135deg, #10b981, #059669); color: #fff; border: none; padding: 12px; border-radius: 10px; font-weight: 700; font-size: 0.95rem; cursor: pointer; box-shadow: 0 4px 12px rgba(16,185,129,0.3); display: flex; align-items: center; justify-content: center; gap: 6px;"> | |
| ✔ Confirmar Recorte | |
| </button> | |
| </div> | |
| </div> | |
| <!-- User Profile Modal (Mobile) --> | |
| <div id="mobProfileModal" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: rgba(10, 10, 15, 0.95); backdrop-filter: blur(12px); flex-direction: column; align-items: center; justify-content: center; padding: 16px; box-sizing: border-box;"> | |
| <div style="width: 100%; max-width: 420px; background: #161622; border: 1px solid rgba(255,255,255,0.12); border-radius: 16px; padding: 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.8);"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 16px;"> | |
| <div style="font-size: 1.1rem; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 6px;"> | |
| 👤 <span>Mi Perfil</span> | |
| </div> | |
| <button type="button" id="btnCloseMobProfile" style="background: rgba(255,255,255,0.1); border: none; color: #fff; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 0.9rem;">✕</button> | |
| </div> | |
| <form id="mobProfileForm" style="display: flex; flex-direction: column; gap: 14px;"> | |
| <div style="display: flex; align-items: center; gap: 12px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.1); padding: 12px; border-radius: 12px;"> | |
| <div style="position: relative; width: 56px; height: 56px; border-radius: 50%; overflow: hidden; border: 2px solid #7c3aed; flex-shrink: 0; background: #000;"> | |
| <img id="mobProfileAvatarPreview" src="" alt="Avatar" style="width: 100%; height: 100%; object-fit: cover;"> | |
| </div> | |
| <div style="flex: 1; display: flex; flex-direction: column; gap: 4px;"> | |
| <label for="mobProfileAvatarInput" style="font-size: 0.75rem; padding: 6px 12px; background: rgba(124, 58, 237, 0.2); border: 1px solid #7c3aed; color: #a78bfa; border-radius: 8px; font-weight: 600; cursor: pointer; display: inline-block; width: fit-content;"> | |
| 📷 Cambiar foto | |
| </label> | |
| <input type="file" id="mobProfileAvatarInput" accept="image/*" style="display: none;"> | |
| <span style="font-size: 0.7rem; color: #a1a1aa;">Formato JPG o PNG</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label style="font-size: 0.8rem; color: #a1a1aa; display: block; margin-bottom: 6px; font-weight: 600;">Apodo / Nickname</label> | |
| <input type="text" id="mobProfileNickInput" required style="width: 100%; box-sizing: border-box; padding: 10px 12px; background: #1e1e24; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; font-size: 0.9rem;"> | |
| </div> | |
| <div> | |
| <label style="font-size: 0.8rem; color: #a1a1aa; display: block; margin-bottom: 6px; font-weight: 600;">Nombre Completo (Opcional)</label> | |
| <input type="text" id="mobProfileFullNameInput" style="width: 100%; box-sizing: border-box; padding: 10px 12px; background: #1e1e24; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; font-size: 0.9rem;"> | |
| </div> | |
| <div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 6px;"> | |
| <button type="button" id="btnCancelMobProfile" style="background: rgba(255,255,255,0.1); border: none; color: #fff; padding: 10px 16px; border-radius: 8px; font-weight: 600; font-size: 0.85rem; cursor: pointer;">Cancelar</button> | |
| <button type="submit" style="background: #7c3aed; border: none; color: #fff; padding: 10px 18px; border-radius: 8px; font-weight: 700; font-size: 0.85rem; cursor: pointer;">Guardar</button> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- Privacy & Security Modal (Mobile) --> | |
| <div id="mobPrivacyModal" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: rgba(10, 10, 15, 0.95); backdrop-filter: blur(12px); flex-direction: column; align-items: center; justify-content: center; padding: 16px; box-sizing: border-box;"> | |
| <div style="width: 100%; max-width: 420px; background: #161622; border: 1px solid rgba(255,255,255,0.12); border-radius: 16px; padding: 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.8);"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 16px;"> | |
| <div style="font-size: 1.1rem; font-weight: 700; color: #ffffff; display: flex; align-items: center; gap: 6px;"> | |
| 🛡️ <span>Privacidad y Seguridad</span> | |
| </div> | |
| <button type="button" id="btnCloseMobPrivacy" style="background: rgba(255,255,255,0.1); border: none; color: #fff; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 0.9rem;">✕</button> | |
| </div> | |
| <form id="mobPrivacyForm" style="display: flex; flex-direction: column; gap: 12px;"> | |
| <div> | |
| <label style="font-size: 0.8rem; color: #a1a1aa; display: block; margin-bottom: 6px; font-weight: 600;">Correo Electrónico</label> | |
| <input type="email" id="mobPrivacyEmailInput" style="width: 100%; box-sizing: border-box; padding: 10px 12px; background: #1e1e24; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; font-size: 0.9rem;"> | |
| </div> | |
| <div style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px; margin-top: 4px;"> | |
| <div style="font-size: 0.85rem; font-weight: 700; color: #fff; margin-bottom: 8px;">Cambiar Contraseña</div> | |
| <div style="margin-bottom: 8px;"> | |
| <label style="font-size: 0.75rem; color: #a1a1aa; display: block; margin-bottom: 4px;">Contraseña Actual</label> | |
| <input type="password" id="mobPrivacyCurrentPasswordInput" style="width: 100%; box-sizing: border-box; padding: 8px 12px; background: #1e1e24; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; font-size: 0.85rem;"> | |
| </div> | |
| <div style="margin-bottom: 8px;"> | |
| <label style="font-size: 0.75rem; color: #a1a1aa; display: block; margin-bottom: 4px;">Nueva Contraseña</label> | |
| <input type="password" id="mobPrivacyNewPasswordInput" style="width: 100%; box-sizing: border-box; padding: 8px 12px; background: #1e1e24; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; font-size: 0.85rem;"> | |
| </div> | |
| <div> | |
| <label style="font-size: 0.75rem; color: #a1a1aa; display: block; margin-bottom: 4px;">Repetir Nueva Contraseña</label> | |
| <input type="password" id="mobPrivacyConfirmPasswordInput" style="width: 100%; box-sizing: border-box; padding: 8px 12px; background: #1e1e24; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; font-size: 0.85rem;"> | |
| </div> | |
| </div> | |
| <div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px;"> | |
| <button type="button" id="btnCancelMobPrivacy" style="background: rgba(255,255,255,0.1); border: none; color: #fff; padding: 10px 16px; border-radius: 8px; font-weight: 600; font-size: 0.85rem; cursor: pointer;">Cancelar</button> | |
| <button type="submit" style="background: #4caf50; border: none; color: #fff; padding: 10px 18px; border-radius: 8px; font-weight: 700; font-size: 0.85rem; cursor: pointer;">Guardar</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <script> | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const modelUrl = urlParams.get('model'); | |
| let currentViewerUrl = ''; | |
| let current2DViewerUrl = ''; | |
| const galleryContainer = document.getElementById('gallery-container'); | |
| const viewerContainer = document.getElementById('viewer-container'); | |
| const viewer = document.getElementById('viewer'); | |
| const overlay = document.getElementById('loading-overlay'); | |
| const loadingText = document.getElementById('loading-text'); | |
| const btnBack = document.getElementById('btnBackToGallery'); | |
| const appHeader = document.getElementById('app-header'); | |
| const galleryGrid = document.getElementById('gallery-grid'); | |
| function exitViewerToGallery() { | |
| console.log("[Viewer] Exiting 3D viewer mode"); | |
| // If page was opened with ?model=... query string, clean redirect to /viewer.html | |
| if (window.location.search && window.location.search.includes('model=')) { | |
| window.location.href = window.location.pathname; | |
| return; | |
| } | |
| if (viewerContainer) viewerContainer.style.display = 'none'; | |
| if (viewer) viewer.src = ''; | |
| if (appHeader) appHeader.style.display = 'flex'; | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| const mobGalleryHeader = document.getElementById('mobGalleryHeader'); | |
| const mobFilterTabs = document.getElementById('mobFilterTabs'); | |
| const navTabGalleryEl = document.getElementById('navTabGallery'); | |
| const navTab3DEl = document.getElementById('navTab3D'); | |
| const navTab2DEl = document.getElementById('navTab2D'); | |
| const creatorContainerEl = document.getElementById('creator-container'); | |
| if (creatorContainerEl) creatorContainerEl.style.display = 'none'; | |
| if (mobNavigation) mobNavigation.style.display = 'flex'; | |
| if (mobGalleryHeader) mobGalleryHeader.style.display = 'block'; | |
| if (mobFilterTabs) mobFilterTabs.style.display = 'flex'; | |
| if (galleryContainer) galleryContainer.style.display = 'flex'; | |
| [navTab3DEl, navTab2DEl, navTabGalleryEl].forEach(btn => { | |
| if (!btn) return; | |
| btn.style.background = 'transparent'; | |
| btn.style.borderColor = 'rgba(255, 255, 255, 0.1)'; | |
| btn.style.color = '#a1a1aa'; | |
| btn.style.boxShadow = 'none'; | |
| }); | |
| if (navTabGalleryEl) { | |
| navTabGalleryEl.style.background = '#7c3aed'; | |
| navTabGalleryEl.style.borderColor = '#7c3aed'; | |
| navTabGalleryEl.style.color = '#ffffff'; | |
| navTabGalleryEl.style.boxShadow = '0 0 10px rgba(124, 58, 237, 0.4)'; | |
| } | |
| fetchGallery(); | |
| } | |
| // Back button to list models | |
| if (btnBack) { | |
| btnBack.onclick = function(e) { | |
| if (e) e.stopPropagation(); | |
| exitViewerToGallery(); | |
| }; | |
| } | |
| const btnDownloadViewerFBX = document.getElementById('btnDownloadViewerFBX'); | |
| if (btnDownloadViewerFBX) { | |
| btnDownloadViewerFBX.onclick = function(e) { | |
| if (e) e.stopPropagation(); | |
| if (!currentViewerUrl) return; | |
| let fbxUrl = currentViewerUrl; | |
| if (fbxUrl.endsWith('.glb')) { | |
| fbxUrl = fbxUrl.replace('.glb', '.fbx'); | |
| } | |
| const a = document.createElement('a'); | |
| a.href = fbxUrl; | |
| a.download = fbxUrl.split('/').pop(); | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| }; | |
| } | |
| function open2DImageViewer(imageUrl) { | |
| current2DViewerUrl = imageUrl; | |
| const fullImg = document.getElementById('full2DImageView'); | |
| if (fullImg) fullImg.src = imageUrl; | |
| if (appHeader) appHeader.style.display = 'none'; | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| if (mobNavigation) mobNavigation.style.display = 'none'; | |
| if (galleryContainer) galleryContainer.style.display = 'none'; | |
| const imgViewer = document.getElementById('image-viewer-container'); | |
| if (imgViewer) imgViewer.style.display = 'flex'; | |
| } | |
| function close2DImageViewer() { | |
| const imgViewer = document.getElementById('image-viewer-container'); | |
| if (imgViewer) imgViewer.style.display = 'none'; | |
| if (appHeader) appHeader.style.display = 'flex'; | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| if (mobNavigation) mobNavigation.style.display = 'flex'; | |
| if (galleryContainer) galleryContainer.style.display = 'flex'; | |
| const navTabGalleryEl = document.getElementById('navTabGallery'); | |
| if (navTabGalleryEl) { | |
| const navTab3DEl = document.getElementById('navTab3D'); | |
| const navTab2DEl = document.getElementById('navTab2D'); | |
| [navTab3DEl, navTab2DEl, navTabGalleryEl].forEach(btn => { | |
| if (!btn) return; | |
| btn.style.background = 'transparent'; | |
| btn.style.borderColor = 'rgba(255, 255, 255, 0.1)'; | |
| btn.style.color = '#a1a1aa'; | |
| btn.style.boxShadow = 'none'; | |
| }); | |
| navTabGalleryEl.style.background = '#7c3aed'; | |
| navTabGalleryEl.style.borderColor = '#7c3aed'; | |
| navTabGalleryEl.style.color = '#ffffff'; | |
| navTabGalleryEl.style.boxShadow = '0 0 10px rgba(124, 58, 237, 0.4)'; | |
| } | |
| } | |
| const btnCreate3DFrom2DViewer = document.getElementById('btnCreate3DFrom2DViewer'); | |
| if (btnCreate3DFrom2DViewer) { | |
| btnCreate3DFrom2DViewer.onclick = function() { | |
| if (!current2DViewerUrl) return; | |
| const imgViewer = document.getElementById('image-viewer-container'); | |
| if (imgViewer) imgViewer.style.display = 'none'; | |
| if (appHeader) appHeader.style.display = 'flex'; | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| if (mobNavigation) mobNavigation.style.display = 'flex'; | |
| selectImageFor3D(current2DViewerUrl); | |
| const navTab3DEl = document.getElementById('navTab3D'); | |
| if (navTab3DEl) { | |
| const navTab2DEl = document.getElementById('navTab2D'); | |
| const navTabGalleryEl = document.getElementById('navTabGallery'); | |
| [navTab3DEl, navTab2DEl, navTabGalleryEl].forEach(btn => { | |
| if (!btn) return; | |
| btn.style.background = 'transparent'; | |
| btn.style.borderColor = 'rgba(255, 255, 255, 0.1)'; | |
| btn.style.color = '#a1a1aa'; | |
| btn.style.boxShadow = 'none'; | |
| }); | |
| navTab3DEl.style.background = '#7c3aed'; | |
| navTab3DEl.style.borderColor = '#7c3aed'; | |
| navTab3DEl.style.color = '#ffffff'; | |
| navTab3DEl.style.boxShadow = '0 0 10px rgba(124, 58, 237, 0.4)'; | |
| } | |
| }; | |
| } | |
| // Load 3D model in viewer | |
| function loadModelInViewer(url) { | |
| currentViewerUrl = url; | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| if (mobNavigation) mobNavigation.style.display = 'none'; | |
| if (galleryContainer) galleryContainer.style.display = 'none'; | |
| const creatorContainerEl = document.getElementById('creator-container'); | |
| if (creatorContainerEl) creatorContainerEl.style.display = 'none'; | |
| if (appHeader) appHeader.style.display = 'none'; | |
| viewerContainer.style.display = 'block'; | |
| overlay.style.display = 'flex'; | |
| overlay.style.opacity = '1'; | |
| loadingText.textContent = "Cargando archivo 3D..."; | |
| // Assign src to model viewer | |
| viewer.src = url; | |
| // Safety timeout: if model is cached or 'load' event fired early, hide overlay after 2 seconds max | |
| setTimeout(() => { | |
| if (overlay.style.display !== 'none') { | |
| overlay.style.opacity = '0'; | |
| setTimeout(() => { overlay.style.display = 'none'; }, 300); | |
| } | |
| }, 2000); | |
| } | |
| viewer.addEventListener('load', () => { | |
| console.log('[Viewer] Model loaded'); | |
| overlay.style.opacity = '0'; | |
| setTimeout(() => { overlay.style.display = 'none'; }, 300); | |
| }); | |
| viewer.addEventListener('error', (e) => { | |
| console.error('Error loading model:', e); | |
| loadingText.innerHTML = '<span style="color: #ef4444;">Error al cargar el archivo 3D.</span>'; | |
| setTimeout(() => { | |
| overlay.style.opacity = '0'; | |
| setTimeout(() => { overlay.style.display = 'none'; }, 300); | |
| }, 3000); | |
| }); | |
| let galleryItems = []; | |
| let currentGalleryFilter = 'all'; | |
| // Library Filter Tabs | |
| const mobFilterAll = document.getElementById('mobFilterAll'); | |
| const mobFilter2D = document.getElementById('mobFilter2D'); | |
| const mobFilter3D = document.getElementById('mobFilter3D'); | |
| function setGalleryFilterActive(activeBtn, filter) { | |
| currentGalleryFilter = filter; | |
| [mobFilterAll, mobFilter2D, mobFilter3D].forEach(btn => { | |
| if (!btn) return; | |
| if (btn === activeBtn) { | |
| btn.style.background = '#7c3aed'; | |
| btn.style.color = '#fff'; | |
| btn.style.fontWeight = '700'; | |
| } else { | |
| btn.style.background = 'transparent'; | |
| btn.style.color = '#a1a1aa'; | |
| btn.style.fontWeight = '600'; | |
| } | |
| }); | |
| renderGallery(); | |
| } | |
| if (mobFilterAll) mobFilterAll.addEventListener('click', () => setGalleryFilterActive(mobFilterAll, 'all')); | |
| if (mobFilter2D) mobFilter2D.addEventListener('click', () => setGalleryFilterActive(mobFilter2D, 'image')); | |
| if (mobFilter3D) mobFilter3D.addEventListener('click', () => setGalleryFilterActive(mobFilter3D, 'model')); | |
| function renderMobileLoginForm(errorMsg = '') { | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| const mobUserCredits = document.getElementById('mobUserCredits'); | |
| const mobGalleryHeader = document.getElementById('mobGalleryHeader'); | |
| const mobFilterTabs = document.getElementById('mobFilterTabs'); | |
| const mobLoginContainer = document.getElementById('mobLoginContainer'); | |
| const galleryContainerEl = document.getElementById('gallery-container'); | |
| const creatorContainerEl = document.getElementById('creator-container'); | |
| const mobError = document.getElementById('mobError'); | |
| if (mobNavigation) mobNavigation.style.display = 'none'; | |
| if (mobUserCredits) mobUserCredits.style.display = 'none'; | |
| if (mobGalleryHeader) mobGalleryHeader.style.display = 'none'; | |
| if (mobFilterTabs) mobFilterTabs.style.display = 'none'; | |
| if (creatorContainerEl) creatorContainerEl.style.display = 'none'; | |
| if (galleryContainerEl) galleryContainerEl.style.display = 'none'; | |
| if (mobLoginContainer) mobLoginContainer.style.display = 'flex'; | |
| if (mobError) { | |
| if (errorMsg) { | |
| mobError.textContent = errorMsg; | |
| mobError.style.display = 'block'; | |
| } else { | |
| mobError.style.display = 'none'; | |
| } | |
| } | |
| } | |
| // Mobile Auth Tab Switcher & Register Form Handlers | |
| const tabMobLogin = document.getElementById('tabMobLogin'); | |
| const tabMobRegister = document.getElementById('tabMobRegister'); | |
| const mobLoginForm = document.getElementById('mobLoginForm'); | |
| const mobRegisterForm = document.getElementById('mobRegisterForm'); | |
| if (tabMobLogin && tabMobRegister) { | |
| tabMobLogin.addEventListener('click', () => { | |
| tabMobLogin.style.background = '#7c3aed'; | |
| tabMobLogin.style.color = '#fff'; | |
| tabMobLogin.style.fontWeight = '700'; | |
| tabMobRegister.style.background = 'transparent'; | |
| tabMobRegister.style.color = '#a1a1aa'; | |
| tabMobRegister.style.fontWeight = '600'; | |
| if (mobLoginForm) mobLoginForm.style.display = 'flex'; | |
| if (mobRegisterForm) mobRegisterForm.style.display = 'none'; | |
| }); | |
| tabMobRegister.addEventListener('click', () => { | |
| tabMobRegister.style.background = '#7c3aed'; | |
| tabMobRegister.style.color = '#fff'; | |
| tabMobRegister.style.fontWeight = '700'; | |
| tabMobLogin.style.background = 'transparent'; | |
| tabMobLogin.style.color = '#a1a1aa'; | |
| tabMobLogin.style.fontWeight = '600'; | |
| if (mobRegisterForm) mobRegisterForm.style.display = 'flex'; | |
| if (mobLoginForm) mobLoginForm.style.display = 'none'; | |
| }); | |
| } | |
| if (mobRegisterForm) { | |
| mobRegisterForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const username = document.getElementById('mobRegUser').value.trim(); | |
| const password = document.getElementById('mobRegPass').value; | |
| const confirmPass = document.getElementById('mobRegPassConfirm').value; | |
| const regErrorEl = document.getElementById('mobRegError'); | |
| const regBtn = document.getElementById('btnMobRegister'); | |
| if (regErrorEl) regErrorEl.style.display = 'none'; | |
| if (password !== confirmPass) { | |
| if (regErrorEl) { | |
| regErrorEl.textContent = 'Las contraseñas no coinciden.'; | |
| regErrorEl.style.display = 'block'; | |
| } | |
| return; | |
| } | |
| if (password.length < 4) { | |
| if (regErrorEl) { | |
| regErrorEl.textContent = 'La contraseña debe tener al menos 4 caracteres.'; | |
| regErrorEl.style.display = 'block'; | |
| } | |
| return; | |
| } | |
| if (regBtn) { | |
| regBtn.disabled = true; | |
| regBtn.textContent = 'Registrando...'; | |
| } | |
| try { | |
| const regRes = await fetch('/api/register', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username, password }) | |
| }); | |
| if (regRes.ok) { | |
| // Registration success, proceed to auto-login | |
| const loginRes = await fetch('/api/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username, password }) | |
| }); | |
| if (loginRes.ok) { | |
| const mobLoginContainer = document.getElementById('mobLoginContainer'); | |
| if (mobLoginContainer) mobLoginContainer.style.display = 'none'; | |
| if (galleryContainer) galleryContainer.style.display = 'flex'; | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| const mobGalleryHeader = document.getElementById('mobGalleryHeader'); | |
| const mobFilterTabs = document.getElementById('mobFilterTabs'); | |
| if (mobNavigation) mobNavigation.style.display = 'flex'; | |
| if (mobGalleryHeader) mobGalleryHeader.style.display = 'block'; | |
| if (mobFilterTabs) mobFilterTabs.style.display = 'flex'; | |
| if (regBtn) { | |
| regBtn.disabled = false; | |
| regBtn.textContent = 'Registrarse'; | |
| } | |
| fetchUserInfo(); | |
| fetchGallery(); | |
| } else { | |
| if (tabMobLogin) tabMobLogin.click(); | |
| } | |
| } else { | |
| const errData = await regRes.json().catch(() => ({})); | |
| if (regBtn) { | |
| regBtn.disabled = false; | |
| regBtn.textContent = 'Registrarse'; | |
| } | |
| if (regErrorEl) { | |
| regErrorEl.textContent = errData.error || 'No se pudo completar el registro'; | |
| regErrorEl.style.display = 'block'; | |
| } | |
| } | |
| } catch (err) { | |
| if (regBtn) { | |
| regBtn.disabled = false; | |
| regBtn.textContent = 'Registrarse'; | |
| } | |
| if (regErrorEl) { | |
| regErrorEl.textContent = 'Error de conexión al intentar registrarse'; | |
| regErrorEl.style.display = 'block'; | |
| } | |
| } | |
| }); | |
| } | |
| if (mobLoginForm) { | |
| mobLoginForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const username = document.getElementById('mobUser').value.trim(); | |
| const password = document.getElementById('mobPass').value; | |
| const errorEl = document.getElementById('mobError'); | |
| const btn = document.getElementById('btnMobLogin'); | |
| if (errorEl) errorEl.style.display = 'none'; | |
| if (btn) { | |
| btn.disabled = true; | |
| btn.textContent = 'Verificando...'; | |
| } | |
| try { | |
| const loginRes = await fetch('/api/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username, password }) | |
| }); | |
| if (loginRes.ok) { | |
| const mobLoginContainer = document.getElementById('mobLoginContainer'); | |
| if (mobLoginContainer) mobLoginContainer.style.display = 'none'; | |
| if (galleryContainer) galleryContainer.style.display = 'flex'; | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| const mobGalleryHeader = document.getElementById('mobGalleryHeader'); | |
| const mobFilterTabs = document.getElementById('mobFilterTabs'); | |
| if (mobNavigation) mobNavigation.style.display = 'flex'; | |
| if (mobGalleryHeader) mobGalleryHeader.style.display = 'block'; | |
| if (mobFilterTabs) mobFilterTabs.style.display = 'flex'; | |
| if (btn) { | |
| btn.disabled = false; | |
| btn.textContent = 'Iniciar Sesión'; | |
| } | |
| fetchUserInfo(); | |
| fetchGallery(); | |
| } else { | |
| const errData = await loginRes.json().catch(() => ({})); | |
| if (btn) { | |
| btn.disabled = false; | |
| btn.textContent = 'Iniciar Sesión'; | |
| } | |
| if (errorEl) { | |
| errorEl.textContent = errData.error || 'Usuario o contraseña incorrectos'; | |
| errorEl.style.display = 'block'; | |
| } | |
| } | |
| } catch (e) { | |
| if (btn) { | |
| btn.disabled = false; | |
| btn.textContent = 'Iniciar Sesión'; | |
| } | |
| if (errorEl) { | |
| errorEl.textContent = 'Error de conexión con el servidor'; | |
| errorEl.style.display = 'block'; | |
| } | |
| } | |
| }); | |
| } | |
| async function fetchGallery() { | |
| try { | |
| const response = await fetch('/api/gallery'); | |
| if (response.status === 401) { | |
| renderMobileLoginForm(); | |
| return; | |
| } | |
| const data = await response.json(); | |
| const mobNavigation = document.getElementById('mobNavigation'); | |
| const mobGalleryHeader = document.getElementById('mobGalleryHeader'); | |
| const mobFilterTabs = document.getElementById('mobFilterTabs'); | |
| const mobLoginContainer = document.getElementById('mobLoginContainer'); | |
| const creatorContainerEl = document.getElementById('creator-container'); | |
| if (mobLoginContainer) mobLoginContainer.style.display = 'none'; | |
| if (mobNavigation) mobNavigation.style.display = 'flex'; | |
| // Only show galleryContainer if Creator panel is not currently active | |
| if (!creatorContainerEl || creatorContainerEl.style.display === 'none') { | |
| if (galleryContainer) galleryContainer.style.display = 'flex'; | |
| if (mobGalleryHeader) mobGalleryHeader.style.display = 'block'; | |
| if (mobFilterTabs) mobFilterTabs.style.display = 'flex'; | |
| } | |
| galleryItems = data.items || []; | |
| renderGallery(); | |
| renderMobGalleryImageGrid(); | |
| checkActiveUserJob(); | |
| } catch (err) { | |
| console.error(err); | |
| if (galleryGrid) { | |
| galleryGrid.innerHTML = `<div style="color: #ef4444; text-align: center; padding: 24px;">Error al conectar con la galería: ${err.message}</div>`; | |
| } | |
| } | |
| } | |
| let isPollingActiveJob = false; | |
| async function checkActiveUserJob() { | |
| if (isPollingActiveJob) return; | |
| try { | |
| const res = await fetch('/api/user-active-job'); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| if (data.has_active && data.job_id) { | |
| isPollingActiveJob = true; | |
| resumeActiveJobPolling(data.job_id, data.type); | |
| } | |
| } catch (e) { | |
| console.warn('[Viewer] Error checking active job:', e); | |
| } | |
| } | |
| function resumeActiveJobPolling(jobId, jobType) { | |
| const mobBgTaskBanner = document.getElementById('mobBgTaskBanner'); | |
| const mobBgTaskText = document.getElementById('mobBgTaskText'); | |
| const typeLabel = jobType === '2d' ? '2D' : '3D'; | |
| if (mobBgTaskBanner && mobBgTaskText) { | |
| mobBgTaskText.textContent = `Generando ${typeLabel}... 0%`; | |
| mobBgTaskBanner.style.display = 'flex'; | |
| } | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const statusRes = await fetch(`/api/job-status?job_id=${jobId}`); | |
| if (!statusRes.ok) return; | |
| const job = await statusRes.json(); | |
| if (job.status === 'processing' || job.status === 'pending') { | |
| const progress = job.progress || 10; | |
| if (mobBgTaskBanner && mobBgTaskText) { | |
| mobBgTaskText.textContent = `Generando ${typeLabel}... ${progress}%`; | |
| mobBgTaskBanner.style.display = 'flex'; | |
| } | |
| } else if (job.status === 'completed') { | |
| clearInterval(pollInterval); | |
| isPollingActiveJob = false; | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| try { fetchGallery(); } catch (e) {} | |
| try { fetchUserInfo(); } catch (e) {} | |
| } else if (job.status === 'failed') { | |
| clearInterval(pollInterval); | |
| isPollingActiveJob = false; | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| } | |
| } catch (e) { | |
| console.warn('[Viewer] Active job poll warning:', e); | |
| } | |
| }, 2500); | |
| } | |
| function renderGallery() { | |
| const filtered = galleryItems.filter(item => { | |
| if (currentGalleryFilter === 'all') return true; | |
| return item.type === currentGalleryFilter; | |
| }); | |
| if (filtered.length === 0) { | |
| galleryGrid.innerHTML = ` | |
| <div style="text-align: center; padding: 40px; color: #a1a1aa; border: 1px dashed rgba(255,255,255,0.08); border-radius: 12px;"> | |
| <div style="font-size: 2.5rem; margin-bottom: 12px;">📁</div> | |
| <div style="font-size: 0.9rem; font-weight: 600; color: #ffffff; margin-bottom: 4px;">No hay elementos guardados</div> | |
| <div style="font-size: 0.8rem; color: #a1a1aa;">Genera una imagen 2D o un modelo 3D para verlo aquí.</div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| galleryGrid.innerHTML = ''; | |
| filtered.forEach(item => { | |
| const date = new Date(item.mtime * 1000).toLocaleString('es-ES', { | |
| day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' | |
| }); | |
| const card = document.createElement('div'); | |
| card.className = 'model-card'; | |
| if (item.type === 'model') { | |
| card.innerHTML = ` | |
| <div class="model-info"> | |
| <div class="model-icon">📦</div> | |
| <div> | |
| <div class="model-name">${item.name.split('/').pop()}</div> | |
| <div class="model-date">${date}</div> | |
| </div> | |
| </div> | |
| <div style="display:flex; gap:6px; align-items:center;"> | |
| <button class="btn-view" style="background:#7c3aed; color:#fff; border:none; padding:8px 12px; border-radius:8px; font-size:1.1rem; cursor:pointer; display:flex; align-items:center; justify-content:center;" title="Visualizar 3D">👁️</button> | |
| <button class="btn-delete" style="background:rgba(239,68,68,0.15); color:#ef4444; border:1px solid rgba(239,68,68,0.3); padding:8px 10px; border-radius:8px; font-size:1rem; cursor:pointer; display:flex; align-items:center; justify-content:center;" title="Eliminar">🗑️</button> | |
| </div> | |
| `; | |
| card.querySelector('.btn-view').addEventListener('click', () => loadModelInViewer(item.url)); | |
| } else { | |
| card.innerHTML = ` | |
| <div class="model-info"> | |
| <img src="${item.url}" style="width:44px; height:44px; object-fit:cover; border-radius:8px; border:1px solid rgba(255,255,255,0.1); flex-shrink:0;"> | |
| <div> | |
| <div class="model-name">${item.name}</div> | |
| <div class="model-date">${date}</div> | |
| </div> | |
| </div> | |
| <div style="display:flex; gap:6px; align-items:center;"> | |
| <button class="btn-view-2d" style="background:#7c3aed; color:#fff; border:none; padding:8px 12px; border-radius:8px; font-size:1.1rem; cursor:pointer; display:flex; align-items:center; justify-content:center;" title="Visualizar Imagen 2D">👁️</button> | |
| <button class="btn-delete" style="background:rgba(239,68,68,0.15); color:#ef4444; border:1px solid rgba(239,68,68,0.3); padding:8px 10px; border-radius:8px; font-size:1rem; cursor:pointer; display:flex; align-items:center; justify-content:center;" title="Eliminar">🗑️</button> | |
| </div> | |
| `; | |
| card.querySelector('.btn-view-2d').addEventListener('click', () => open2DImageViewer(item.url)); | |
| } | |
| const btnDel = card.querySelector('.btn-delete'); | |
| if (btnDel) { | |
| btnDel.addEventListener('click', async () => { | |
| if (confirm(`¿Eliminar ${item.name}?`)) { | |
| try { | |
| const res = await fetch(`/api/gallery-item?name=${encodeURIComponent(item.name)}&type=${item.type}`, { method: 'DELETE' }); | |
| if (res.ok) fetchGallery(); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| }); | |
| } | |
| galleryGrid.appendChild(card); | |
| }); | |
| } | |
| function renderMobGalleryImageGrid() { | |
| const grid = document.getElementById('mobGalleryImageGrid'); | |
| if (!grid) return; | |
| const images = galleryItems.filter(item => item.type === 'image'); | |
| if (images.length === 0) { | |
| grid.innerHTML = `<div style="color:#a1a1aa; font-size:0.8rem; grid-column:span 2; text-align:center; padding:20px;">No tienes imágenes 2D guardadas.<br>Genera una en la sección "Generar 2D".</div>`; | |
| return; | |
| } | |
| grid.innerHTML = ''; | |
| images.forEach(img => { | |
| const itemDiv = document.createElement('div'); | |
| itemDiv.style.cssText = 'position:relative; cursor:pointer; border-radius:8px; overflow:hidden; border:2px solid transparent; aspect-ratio:1; transition:all 0.2s;'; | |
| itemDiv.innerHTML = `<img src="${img.url}" style="width:100%; height:100%; object-fit:cover;">`; | |
| itemDiv.addEventListener('click', () => { | |
| grid.querySelectorAll('div').forEach(d => d.style.borderColor = 'transparent'); | |
| itemDiv.style.borderColor = '#10b981'; | |
| selectImageFor3D(img.url); | |
| }); | |
| grid.appendChild(itemDiv); | |
| }); | |
| } | |
| function selectImageFor3D(imageUrl) { | |
| base64Image = imageUrl; | |
| imagePreviewMobile.src = imageUrl; | |
| dropzone.style.display = 'none'; | |
| imagePreviewContainer.style.display = 'block'; | |
| btnCreate3DMobile.style.display = 'flex'; | |
| // Show creator 3D view | |
| if (navTab3D) setMobNavActive(navTab3D); | |
| galleryContainer.style.display = 'none'; | |
| creatorContainer.style.display = 'flex'; | |
| // Select Upload tab panel | |
| if (tabUpload) tabUpload.click(); | |
| } | |
| // Creator script variables | |
| let activeCreatorTab = 'upload'; | |
| let base64Image = null; | |
| const creatorContainer = document.getElementById('creator-container'); | |
| const tabUpload = document.getElementById('tabUpload'); | |
| const tabGallerySelect = document.getElementById('tabGallerySelect'); | |
| const panelUpload = document.getElementById('panelUpload'); | |
| const panelGallerySelect = document.getElementById('panelGallerySelect'); | |
| const panelPrompt = document.getElementById('panelPrompt'); | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInputMobile = document.getElementById('fileInputMobile'); | |
| const imagePreviewContainer = document.getElementById('imagePreviewContainer'); | |
| const imagePreviewMobile = document.getElementById('imagePreviewMobile'); | |
| const btnRemoveImageMobile = document.getElementById('btnRemoveImageMobile'); | |
| const promptInputMobile = document.getElementById('promptInputMobile'); | |
| const btnGen2DMobile = document.getElementById('btnGen2DMobile'); | |
| const aiImageContainer = document.getElementById('aiImageContainer'); | |
| const aiImageMobile = document.getElementById('aiImageMobile'); | |
| const btnCreate3DFrom2DGenerated = document.getElementById('btnCreate3DFrom2DGenerated'); | |
| const btnCreate3DMobile = document.getElementById('btnCreate3DMobile'); | |
| // Navigation between creator and gallery tabs | |
| const navTab3D = document.getElementById('navTab3D'); | |
| const navTab2D = document.getElementById('navTab2D'); | |
| const navTabGallery = document.getElementById('navTabGallery'); | |
| function setMobNavActive(activeBtn) { | |
| [navTab3D, navTab2D, navTabGallery].forEach(btn => { | |
| if (!btn) return; | |
| if (btn === activeBtn) { | |
| btn.style.background = '#7c3aed'; | |
| btn.style.borderColor = '#7c3aed'; | |
| btn.style.color = '#ffffff'; | |
| btn.style.boxShadow = '0 0 10px rgba(124, 58, 237, 0.4)'; | |
| } else { | |
| btn.style.background = 'transparent'; | |
| btn.style.borderColor = 'rgba(255, 255, 255, 0.1)'; | |
| btn.style.color = '#a1a1aa'; | |
| btn.style.boxShadow = 'none'; | |
| } | |
| }); | |
| } | |
| if (navTab3D) { | |
| navTab3D.addEventListener('click', () => { | |
| setMobNavActive(navTab3D); | |
| galleryContainer.style.display = 'none'; | |
| creatorContainer.style.display = 'flex'; | |
| document.getElementById('creatorSectionTitle').textContent = 'Crear Modelo 3D'; | |
| document.getElementById('creatorTabHeader').style.display = 'flex'; | |
| if (tabUpload) tabUpload.click(); | |
| }); | |
| } | |
| if (navTab2D) { | |
| navTab2D.addEventListener('click', () => { | |
| setMobNavActive(navTab2D); | |
| galleryContainer.style.display = 'none'; | |
| creatorContainer.style.display = 'flex'; | |
| document.getElementById('creatorSectionTitle').textContent = 'Generar Imagen 2D con IA'; | |
| document.getElementById('creatorTabHeader').style.display = 'none'; | |
| panelUpload.style.display = 'none'; | |
| if (panelGallerySelect) panelGallerySelect.style.display = 'none'; | |
| panelPrompt.style.display = 'flex'; | |
| btnCreate3DMobile.style.display = 'none'; | |
| }); | |
| } | |
| if (navTabGallery) { | |
| navTabGallery.addEventListener('click', () => { | |
| setMobNavActive(navTabGallery); | |
| creatorContainer.style.display = 'none'; | |
| galleryContainer.style.display = 'flex'; | |
| fetchGallery(); | |
| }); | |
| } | |
| function resetCreatorState() { | |
| base64Image = null; | |
| if (fileInputMobile) fileInputMobile.value = ''; | |
| if (imagePreviewContainer) imagePreviewContainer.style.display = 'none'; | |
| if (dropzone) dropzone.style.display = 'flex'; | |
| if (promptInputMobile) promptInputMobile.value = ''; | |
| if (aiImageContainer) aiImageContainer.style.display = 'none'; | |
| if (btnCreate3DMobile) { | |
| btnCreate3DMobile.disabled = false; | |
| btnCreate3DMobile.style.display = 'none'; | |
| } | |
| } | |
| // Sub-tab switcher inside Crear Modelo 3D | |
| if (tabUpload && tabGallerySelect) { | |
| tabUpload.addEventListener('click', () => { | |
| tabUpload.style.background = '#27272a'; | |
| tabUpload.style.color = '#fff'; | |
| tabGallerySelect.style.background = 'transparent'; | |
| tabGallerySelect.style.color = '#a1a1aa'; | |
| panelUpload.style.display = 'flex'; | |
| panelGallerySelect.style.display = 'none'; | |
| panelPrompt.style.display = 'none'; | |
| btnCreate3DMobile.style.display = imagePreviewContainer.style.display === 'block' ? 'flex' : 'none'; | |
| }); | |
| tabGallerySelect.addEventListener('click', () => { | |
| tabGallerySelect.style.background = '#27272a'; | |
| tabGallerySelect.style.color = '#fff'; | |
| tabUpload.style.background = 'transparent'; | |
| tabUpload.style.color = '#a1a1aa'; | |
| panelGallerySelect.style.display = 'flex'; | |
| panelUpload.style.display = 'none'; | |
| panelPrompt.style.display = 'none'; | |
| renderMobGalleryImageGrid(); | |
| btnCreate3DMobile.style.display = base64Image ? 'flex' : 'none'; | |
| }); | |
| } | |
| // Upload handlers | |
| if (dropzone && fileInputMobile) { | |
| dropzone.addEventListener('click', () => { | |
| fileInputMobile.click(); | |
| }); | |
| fileInputMobile.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| base64Image = event.target.result; | |
| imagePreviewMobile.src = base64Image; | |
| dropzone.style.display = 'none'; | |
| imagePreviewContainer.style.display = 'block'; | |
| btnCreate3DMobile.style.display = 'flex'; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| if (btnRemoveImageMobile) { | |
| btnRemoveImageMobile.addEventListener('click', () => { | |
| base64Image = null; | |
| if (fileInputMobile) fileInputMobile.value = ''; | |
| if (cameraInputMobile) cameraInputMobile.value = ''; | |
| imagePreviewContainer.style.display = 'none'; | |
| dropzone.style.display = 'flex'; | |
| btnCreate3DMobile.style.display = 'none'; | |
| }); | |
| } | |
| // Cropper.js Integration for Mobile View | |
| let mobileCropper = null; | |
| const btnOpenCropMobile = document.getElementById('btnOpenCropMobile'); | |
| const cropModalOverlay = document.getElementById('cropModalOverlay'); | |
| const cropImageTarget = document.getElementById('cropImageTarget'); | |
| const btnCancelCrop = document.getElementById('btnCancelCrop'); | |
| const btnApplyCrop = document.getElementById('btnApplyCrop'); | |
| const btnCropAspectFree = document.getElementById('btnCropAspectFree'); | |
| const btnCropAspectSquare = document.getElementById('btnCropAspectSquare'); | |
| const btnCropReset = document.getElementById('btnCropReset'); | |
| function initCropperMobile(imageUrl) { | |
| if (!cropModalOverlay || !cropImageTarget) return; | |
| cropImageTarget.src = imageUrl; | |
| cropModalOverlay.style.display = 'flex'; | |
| if (mobileCropper) { | |
| mobileCropper.destroy(); | |
| mobileCropper = null; | |
| } | |
| setTimeout(() => { | |
| mobileCropper = new Cropper(cropImageTarget, { | |
| aspectRatio: 1, | |
| viewMode: 1, | |
| autoCropArea: 0.85, | |
| responsive: true, | |
| background: false | |
| }); | |
| }, 150); | |
| } | |
| if (btnOpenCropMobile) { | |
| btnOpenCropMobile.addEventListener('click', () => { | |
| if (base64Image) { | |
| initCropperMobile(base64Image); | |
| } | |
| }); | |
| } | |
| if (btnCancelCrop) { | |
| btnCancelCrop.addEventListener('click', () => { | |
| if (cropModalOverlay) cropModalOverlay.style.display = 'none'; | |
| if (mobileCropper) { | |
| mobileCropper.destroy(); | |
| mobileCropper = null; | |
| } | |
| }); | |
| } | |
| if (btnCropAspectFree) { | |
| btnCropAspectFree.addEventListener('click', () => { | |
| if (mobileCropper) mobileCropper.setAspectRatio(NaN); | |
| }); | |
| } | |
| if (btnCropAspectSquare) { | |
| btnCropAspectSquare.addEventListener('click', () => { | |
| if (mobileCropper) mobileCropper.setAspectRatio(1); | |
| }); | |
| } | |
| if (btnCropReset) { | |
| btnCropReset.addEventListener('click', () => { | |
| if (mobileCropper) mobileCropper.reset(); | |
| }); | |
| } | |
| if (btnApplyCrop) { | |
| btnApplyCrop.addEventListener('click', () => { | |
| if (!mobileCropper) return; | |
| const croppedCanvas = mobileCropper.getCroppedCanvas({ | |
| width: 1024, | |
| height: 1024, | |
| imageSmoothingEnabled: true, | |
| imageSmoothingQuality: 'high' | |
| }); | |
| if (croppedCanvas) { | |
| base64Image = croppedCanvas.toDataURL('image/png'); | |
| if (imagePreviewMobile) imagePreviewMobile.src = base64Image; | |
| if (cropModalOverlay) cropModalOverlay.style.display = 'none'; | |
| if (mobileCropper) { | |
| mobileCropper.destroy(); | |
| mobileCropper = null; | |
| } | |
| } | |
| }); | |
| } | |
| // 2D AI Generation | |
| if (btnGen2DMobile) { | |
| btnGen2DMobile.addEventListener('click', async () => { | |
| const prompt = promptInputMobile.value.trim(); | |
| if (!prompt) { | |
| alert('Por favor escribe una descripción para la imagen.'); | |
| return; | |
| } | |
| btnGen2DMobile.disabled = true; | |
| btnGen2DMobile.textContent = 'Generando...'; | |
| creatorStatus.style.display = 'block'; | |
| creatorStatusTitle.textContent = 'Generando Imagen 2D...'; | |
| creatorStatusDesc.textContent = 'Esto puede tardar unos segundos. Por favor, espera.'; | |
| const mobBgTaskBanner = document.getElementById('mobBgTaskBanner'); | |
| const mobBgTaskText = document.getElementById('mobBgTaskText'); | |
| try { | |
| const response = await fetch('/api/generate-2d', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ prompt, model: 'stabilityai/stable-diffusion-3.5-large', negative_prompt: '', token: '' }) | |
| }); | |
| if (response.status === 402) { | |
| const err = await response.json().catch(() => ({})); | |
| throw new Error(err.error || 'Créditos insuficientes para generar la imagen (Cuesta 1 crédito).'); | |
| } | |
| if (response.status !== 202) { | |
| throw new Error('Error al iniciar la generación de la imagen.'); | |
| } | |
| const initData = await response.json(); | |
| const jobId = initData.job_id; | |
| if (mobBgTaskBanner && mobBgTaskText) { | |
| mobBgTaskText.textContent = `Generando 2D... 0%`; | |
| mobBgTaskBanner.style.display = 'flex'; | |
| } | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const statusRes = await fetch(`/api/job-status?job_id=${jobId}`); | |
| if (!statusRes.ok) return; | |
| const job = await statusRes.json(); | |
| if (job.status === 'processing' || job.status === 'pending') { | |
| const progress = job.progress || 10; | |
| if (mobBgTaskBanner && mobBgTaskText) { | |
| mobBgTaskText.textContent = `Generando 2D... ${progress}%`; | |
| mobBgTaskBanner.style.display = 'flex'; | |
| } | |
| } else if (job.status === 'completed') { | |
| clearInterval(pollInterval); | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| base64Image = job.result.image; | |
| aiImageMobile.src = base64Image; | |
| aiImageContainer.style.display = 'flex'; | |
| btnGen2DMobile.disabled = false; | |
| btnGen2DMobile.textContent = 'Generar Otra Imagen (Cuesta 1 crédito)'; | |
| creatorStatus.style.display = 'none'; | |
| // Refresh gallery & credits | |
| fetchGallery(); | |
| fetchUserInfo(); | |
| } else if (job.status === 'failed') { | |
| clearInterval(pollInterval); | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| throw new Error(job.error || 'La generación de imagen falló.'); | |
| } | |
| } catch (err) { | |
| clearInterval(pollInterval); | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| btnGen2DMobile.disabled = false; | |
| btnGen2DMobile.textContent = 'Generar Imagen 2D (Cuesta 1 crédito)'; | |
| creatorStatus.style.display = 'none'; | |
| alert('Error: ' + err.message); | |
| } | |
| }, 2000); | |
| } catch (err) { | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| btnGen2DMobile.disabled = false; | |
| btnGen2DMobile.textContent = 'Generar Imagen 2D (Cuesta 1 crédito)'; | |
| creatorStatus.style.display = 'none'; | |
| alert('Error: ' + err.message); | |
| } | |
| }); | |
| } | |
| // 3D Model Generation | |
| if (btnCreate3DMobile) { | |
| btnCreate3DMobile.addEventListener('click', async () => { | |
| if (!base64Image) { | |
| alert('Por favor, toma/sube una foto o genera una imagen con IA antes de crear el modelo 3D.'); | |
| return; | |
| } | |
| const mobBgTaskBanner = document.getElementById('mobBgTaskBanner'); | |
| const mobBgTaskText = document.getElementById('mobBgTaskText'); | |
| if (mobBgTaskBanner && mobBgTaskText) { | |
| mobBgTaskText.textContent = `Generando 3D... 0%`; | |
| mobBgTaskBanner.style.display = 'flex'; | |
| } | |
| try { | |
| let response = null; | |
| let initRetries = 0; | |
| const maxInitRetries = 3; | |
| while (initRetries < maxInitRetries) { | |
| try { | |
| response = await fetch('/api/generate-3d', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| version: 'v2', | |
| image: base64Image, | |
| token: '', | |
| seed: 42, | |
| resolution: '1536', | |
| decimation_target: 100000, | |
| texture_size: 4096, | |
| ss_guidance: 7.5, | |
| ss_steps: 12, | |
| slat_guidance: 3.0, | |
| slat_steps: 12, | |
| auto_optimize: false, | |
| quad_target_faces: 60000, | |
| remeshMethod: 'cleanup', | |
| prompt: '' | |
| }) | |
| }); | |
| break; | |
| } catch (netErr) { | |
| initRetries++; | |
| if (initRetries >= maxInitRetries) throw netErr; | |
| console.warn(`[Init 3D Retry ${initRetries}/${maxInitRetries}] Error de conexión, reintentando...`); | |
| await new Promise(r => setTimeout(r, 1500)); | |
| } | |
| } | |
| const resJson = await response.json().catch(() => ({})); | |
| if (response.status === 402) { | |
| throw new Error(resJson.error || 'Créditos insuficientes para crear el modelo 3D (Cuesta 5 créditos).'); | |
| } | |
| if (response.status !== 202) { | |
| throw new Error(resJson.error || resJson.message || 'Error al iniciar la generación del modelo 3D.'); | |
| } | |
| const jobId = resJson.job_id; | |
| // Refresh credits immediately since they are charged on job initiation | |
| fetchUserInfo(); | |
| if (mobBgTaskBanner && mobBgTaskText) { | |
| mobBgTaskText.textContent = `Generando 3D... 0%`; | |
| mobBgTaskBanner.style.display = 'flex'; | |
| } | |
| let consecutiveFailures = 0; | |
| const maxAllowedFailures = 12; | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const statusRes = await fetch(`/api/job-status?job_id=${jobId}`); | |
| if (!statusRes.ok) { | |
| consecutiveFailures++; | |
| if (consecutiveFailures < maxAllowedFailures) return; | |
| throw new Error('No se pudo obtener respuesta del servidor tras varios intentos.'); | |
| } | |
| consecutiveFailures = 0; | |
| const job = await statusRes.json(); | |
| if (job.status === 'processing' || job.status === 'pending') { | |
| const progress = job.progress || 10; | |
| if (mobBgTaskBanner && mobBgTaskText) { | |
| mobBgTaskText.textContent = `Generando 3D... ${progress}%`; | |
| mobBgTaskBanner.style.display = 'flex'; | |
| } | |
| } else if (job.status === 'completed') { | |
| clearInterval(pollInterval); | |
| btnCreate3DMobile.disabled = false; | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| resetCreatorState(); | |
| try { fetchGallery(); } catch (e) {} | |
| try { fetchUserInfo(); } catch (e) {} | |
| const resData = job.result; | |
| const modelUrl = resData ? (resData.gltfUrl || resData.glbUrl) : null; | |
| if (modelUrl) { | |
| try { | |
| loadModelInViewer(modelUrl); | |
| } catch (errLoad) { | |
| console.error("Error cargando visor:", errLoad); | |
| alert('¡Modelo 3D creado con éxito! Se ha añadido a tu biblioteca.'); | |
| } | |
| } else { | |
| alert('¡Modelo 3D creado con éxito! Se ha añadido a tu biblioteca.'); | |
| } | |
| } else if (job.status === 'failed') { | |
| clearInterval(pollInterval); | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| throw new Error(job.message || job.error || 'La generación 3D falló.'); | |
| } | |
| } catch (err) { | |
| const errStr = (err.message || '' ) + (err.name || ''); | |
| if (errStr.includes('TypeError') || errStr.includes('Failed to fetch') || errStr.includes('NetworkError') || errStr.includes('fetch')) { | |
| consecutiveFailures++; | |
| if (consecutiveFailures < maxAllowedFailures) { | |
| console.warn(`[Network Retry ${consecutiveFailures}/${maxAllowedFailures}] Reconectando a la tarea 3D...`); | |
| return; | |
| } | |
| } | |
| clearInterval(pollInterval); | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| btnCreate3DMobile.disabled = false; | |
| alert('Error al generar modelo 3D: ' + err.message); | |
| fetchUserInfo(); | |
| } | |
| }, 2500); | |
| } catch (err) { | |
| if (mobBgTaskBanner) mobBgTaskBanner.style.display = 'none'; | |
| btnCreate3DMobile.disabled = false; | |
| alert('Error: ' + err.message); | |
| fetchUserInfo(); | |
| } | |
| }); | |
| } | |
| // Fetch User Info (Username and Credits) | |
| async function fetchUserInfo() { | |
| try { | |
| const response = await fetch('/api/me'); | |
| const mobUserCredits = document.getElementById('mobUserCredits'); | |
| const mobUsername = document.getElementById('mobUsername'); | |
| const mobCredits = document.getElementById('mobCredits'); | |
| const mobUserAvatarImg = document.getElementById('mobUserAvatarImg'); | |
| const mobUserAvatarInitial = document.getElementById('mobUserAvatarInitial'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| if (data.username) { | |
| const displayName = data.nick || data.username; | |
| if (mobUsername) mobUsername.textContent = displayName; | |
| if (mobCredits) mobCredits.textContent = data.credits || 0; | |
| if (data.avatar && (data.avatar.startsWith('data:image') || data.avatar.startsWith('http') || data.avatar.startsWith('/'))) { | |
| if (mobUserAvatarImg) { | |
| mobUserAvatarImg.src = data.avatar; | |
| mobUserAvatarImg.style.display = 'block'; | |
| } | |
| if (mobUserAvatarInitial) mobUserAvatarInitial.style.display = 'none'; | |
| } else { | |
| if (mobUserAvatarImg) mobUserAvatarImg.style.display = 'none'; | |
| if (mobUserAvatarInitial) { | |
| mobUserAvatarInitial.textContent = displayName.charAt(0).toUpperCase(); | |
| mobUserAvatarInitial.style.display = 'block'; | |
| } | |
| } | |
| if (mobUserCredits) mobUserCredits.style.display = 'block'; | |
| } else { | |
| if (mobUserCredits) mobUserCredits.style.display = 'none'; | |
| renderMobileLoginForm(); | |
| } | |
| } else { | |
| if (mobUserCredits) mobUserCredits.style.display = 'none'; | |
| renderMobileLoginForm(); | |
| } | |
| } catch (err) { | |
| console.error("Error fetching user info:", err); | |
| } | |
| } | |
| // Desktop redirection force setter | |
| const btnGoToDesktop = document.getElementById('btnGoToDesktop'); | |
| if (btnGoToDesktop) { | |
| btnGoToDesktop.addEventListener('click', () => { | |
| sessionStorage.setItem('forceDesktop', 'true'); | |
| sessionStorage.setItem('dismissedMobileWarning', 'true'); | |
| }); | |
| } | |
| // User Menu Dropdown Handler | |
| const btnMobUserMenu = document.getElementById('btnMobUserMenu'); | |
| const mobUserDropdown = document.getElementById('mobUserDropdown'); | |
| if (btnMobUserMenu && mobUserDropdown) { | |
| btnMobUserMenu.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const isVisible = mobUserDropdown.style.display === 'block'; | |
| mobUserDropdown.style.display = isVisible ? 'none' : 'block'; | |
| }); | |
| document.addEventListener('click', (e) => { | |
| if (!btnMobUserMenu.contains(e.target) && !mobUserDropdown.contains(e.target)) { | |
| mobUserDropdown.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| // Mobile Profile and Privacy Modals Bindings | |
| const btnMobProfile = document.getElementById('btnMobProfile'); | |
| const btnMobPrivacy = document.getElementById('btnMobPrivacy'); | |
| const mobProfileModal = document.getElementById('mobProfileModal'); | |
| const mobPrivacyModal = document.getElementById('mobPrivacyModal'); | |
| const btnCloseMobProfile = document.getElementById('btnCloseMobProfile'); | |
| const btnCancelMobProfile = document.getElementById('btnCancelMobProfile'); | |
| const btnCloseMobPrivacy = document.getElementById('btnCloseMobPrivacy'); | |
| const btnCancelMobPrivacy = document.getElementById('btnCancelMobPrivacy'); | |
| const mobProfileForm = document.getElementById('mobProfileForm'); | |
| const mobPrivacyForm = document.getElementById('mobPrivacyForm'); | |
| const mobProfileAvatarInput = document.getElementById('mobProfileAvatarInput'); | |
| const mobProfileAvatarPreview = document.getElementById('mobProfileAvatarPreview'); | |
| if (btnMobProfile) { | |
| btnMobProfile.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| if (mobUserDropdown) mobUserDropdown.style.display = 'none'; | |
| fetch('/api/me').then(res => res.json()).then(data => { | |
| const nickInput = document.getElementById('mobProfileNickInput'); | |
| const fullNameInput = document.getElementById('mobProfileFullNameInput'); | |
| if (nickInput) nickInput.value = data.nick || data.username || ''; | |
| if (fullNameInput) fullNameInput.value = data.full_name || ''; | |
| if (mobProfileAvatarPreview && data.avatar) mobProfileAvatarPreview.src = data.avatar; | |
| if (mobProfileModal) mobProfileModal.style.display = 'flex'; | |
| }).catch(() => { | |
| if (mobProfileModal) mobProfileModal.style.display = 'flex'; | |
| }); | |
| }); | |
| } | |
| if (btnMobPrivacy) { | |
| btnMobPrivacy.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| if (mobUserDropdown) mobUserDropdown.style.display = 'none'; | |
| fetch('/api/me').then(res => res.json()).then(data => { | |
| const emailInput = document.getElementById('mobPrivacyEmailInput'); | |
| if (emailInput) emailInput.value = data.email || ''; | |
| if (mobPrivacyModal) mobPrivacyModal.style.display = 'flex'; | |
| }).catch(() => { | |
| if (mobPrivacyModal) mobPrivacyModal.style.display = 'flex'; | |
| }); | |
| }); | |
| } | |
| if (mobProfileAvatarInput && mobProfileAvatarPreview) { | |
| mobProfileAvatarInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (evt) => { | |
| mobProfileAvatarPreview.src = evt.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| } | |
| [btnCloseMobProfile, btnCancelMobProfile].forEach(btn => { | |
| if (btn) btn.addEventListener('click', () => mobProfileModal.style.display = 'none'); | |
| }); | |
| [btnCloseMobPrivacy, btnCancelMobPrivacy].forEach(btn => { | |
| if (btn) btn.addEventListener('click', () => mobPrivacyModal.style.display = 'none'); | |
| }); | |
| if (mobProfileForm) { | |
| mobProfileForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const nick = document.getElementById('mobProfileNickInput')?.value.trim(); | |
| const full_name = document.getElementById('mobProfileFullNameInput')?.value.trim(); | |
| const avatar = mobProfileAvatarPreview?.src; | |
| try { | |
| const res = await fetch('/api/update-profile', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ nick, full_name, avatar }) | |
| }); | |
| if (res.ok) { | |
| alert('¡Perfil actualizado con éxito!'); | |
| if (mobProfileModal) mobProfileModal.style.display = 'none'; | |
| fetchUserInfo(); | |
| } else { | |
| alert('Error al actualizar el perfil.'); | |
| } | |
| } catch (err) { | |
| alert('Error de conexión al actualizar perfil.'); | |
| } | |
| }); | |
| } | |
| if (mobPrivacyForm) { | |
| mobPrivacyForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const email = document.getElementById('mobPrivacyEmailInput')?.value.trim(); | |
| const current_password = document.getElementById('mobPrivacyCurrentPasswordInput')?.value; | |
| const new_password = document.getElementById('mobPrivacyNewPasswordInput')?.value; | |
| const confirm_password = document.getElementById('mobPrivacyConfirmPasswordInput')?.value; | |
| if (new_password && new_password !== confirm_password) { | |
| alert('Las contraseñas no coinciden.'); | |
| return; | |
| } | |
| try { | |
| const res = await fetch('/api/update-privacy', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, current_password, new_password }) | |
| }); | |
| const data = await res.json(); | |
| if (res.ok && data.success) { | |
| alert('¡Configuración de privacidad guardada!'); | |
| if (mobPrivacyModal) mobPrivacyModal.style.display = 'none'; | |
| } else { | |
| alert(data.error || 'Error al guardar privacidad.'); | |
| } | |
| } catch (err) { | |
| alert('Error de conexión al guardar privacidad.'); | |
| } | |
| }); | |
| } | |
| // Logout handler | |
| const btnMobLogout = document.getElementById('btnMobLogout'); | |
| if (btnMobLogout) { | |
| btnMobLogout.addEventListener('click', async () => { | |
| if (confirm('¿Estás seguro de que deseas cerrar sesión?')) { | |
| try { | |
| document.cookie = "session_user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; | |
| document.cookie = "session_user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure"; | |
| await fetch('/api/logout', { method: 'POST' }); | |
| if (mobUserDropdown) mobUserDropdown.style.display = 'none'; | |
| renderMobileLoginForm(); | |
| } catch (e) { | |
| console.error("Error logging out:", e); | |
| renderMobileLoginForm(); | |
| } | |
| } | |
| }); | |
| } | |
| // Routing Logic | |
| if (modelUrl) { | |
| loadModelInViewer(modelUrl); | |
| } else { | |
| fetchGallery(); | |
| fetchUserInfo(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |