LogicalTrue commited on
Commit
1b34c4c
·
1 Parent(s): 439300a

Optimize mobile experience: Auto-detect viewport and redirect to interactive mobile 3D gallery and visualizer

Browse files
Files changed (2) hide show
  1. frontend/app.js +61 -0
  2. frontend/viewer.html +249 -35
frontend/app.js CHANGED
@@ -3932,3 +3932,64 @@ function updateSelectedBoneMarker() {
3932
  }
3933
  }
3934
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3932
  }
3933
  }
3934
 
3935
+ // Check mobile viewport and show interactive redirect dialog
3936
+ function checkMobileRedirect() {
3937
+ if (window.innerWidth < 768) {
3938
+ const overlay = document.createElement('div');
3939
+ overlay.style.position = 'fixed';
3940
+ overlay.style.top = '0';
3941
+ overlay.style.left = '0';
3942
+ overlay.style.width = '100vw';
3943
+ overlay.style.height = '100vh';
3944
+ overlay.style.backgroundColor = 'rgba(10, 10, 12, 0.96)';
3945
+ overlay.style.zIndex = '999999';
3946
+ overlay.style.display = 'flex';
3947
+ overlay.style.alignItems = 'center';
3948
+ overlay.style.justifyContent = 'center';
3949
+ overlay.style.padding = '24px';
3950
+ overlay.style.boxSizing = 'border-box';
3951
+ overlay.style.backdropFilter = 'blur(10px)';
3952
+ overlay.style.webkitBackdropFilter = 'blur(10px)';
3953
+
3954
+ overlay.innerHTML = `
3955
+ <div style="background: #18181c; border: 1px solid #7c3aed; border-radius: 16px; padding: 24px; text-align: center; max-width: 400px; width: 100%; box-shadow: 0 20px 45px rgba(0,0,0,0.5); font-family: sans-serif;">
3956
+ <div style="font-size: 3.5rem; margin-bottom: 16px; animation: bounce 2s infinite;">📱</div>
3957
+ <h3 style="color: #ffffff; margin-top: 0; font-size: 1.35rem; font-weight: 700; margin-bottom: 12px; letter-spacing: -0.5px;">¿Estás en un Celular?</h3>
3958
+ <p style="color: #a1a1aa; font-size: 0.9rem; line-height: 1.5; margin-bottom: 24px;">
3959
+ La creación, edición y limpieza de mallas está diseñada para pantallas de PC/Laptops. En celulares, te recomendamos usar el visor 3D táctil optimizado y proyectar en Realidad Aumentada (AR).
3960
+ </p>
3961
+ <div style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
3962
+ <a href="/viewer.html" style="background: #7c3aed; color: #ffffff; padding: 14px; border-radius: 10px; text-decoration: none; font-weight: 700; font-size: 0.95rem; display: block; box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);">
3963
+ Abrir Visor Móvil (AR)
3964
+ </a>
3965
+ <button id="btnStayOnPCWeb" style="background: transparent; border: 1px solid rgba(255,255,255,0.15); color: #a1a1aa; padding: 12px; border-radius: 10px; font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: all 0.2s;">
3966
+ Seguir en Web de PC (No recomendado)
3967
+ </button>
3968
+ </div>
3969
+ </div>
3970
+ <style>
3971
+ @keyframes bounce {
3972
+ 0%, 100% { transform: translateY(0); }
3973
+ 50% { transform: translateY(-10px); }
3974
+ }
3975
+ #btnStayOnPCWeb:hover {
3976
+ background: rgba(255,255,255,0.05);
3977
+ color: #ffffff;
3978
+ }
3979
+ </style>
3980
+ `;
3981
+ document.body.appendChild(overlay);
3982
+
3983
+ overlay.querySelector('#btnStayOnPCWeb').addEventListener('click', () => {
3984
+ document.body.removeChild(overlay);
3985
+ });
3986
+ }
3987
+ }
3988
+
3989
+ // Execute check on page load
3990
+ if (document.readyState === 'loading') {
3991
+ document.addEventListener('DOMContentLoaded', checkMobileRedirect);
3992
+ } else {
3993
+ checkMobileRedirect();
3994
+ }
3995
+
frontend/viewer.html CHANGED
@@ -32,6 +32,8 @@
32
  left: 0;
33
  right: 0;
34
  z-index: 10;
 
 
35
  }
36
 
37
  .logo {
@@ -52,6 +54,14 @@
52
  text-decoration: none;
53
  }
54
 
 
 
 
 
 
 
 
 
55
  model-viewer {
56
  width: 100%;
57
  height: 100%;
@@ -62,23 +72,20 @@
62
  #ar-button {
63
  background-color: #7c3aed;
64
  color: white;
65
- border-radius: 4px;
66
  border: none;
67
  position: absolute;
68
  bottom: 24px;
69
  right: 24px;
70
- padding: 12px 18px;
71
  font-size: 0.95rem;
72
  font-weight: 600;
73
  cursor: pointer;
74
- box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
75
  display: flex;
76
  align-items: center;
77
  gap: 8px;
78
- }
79
-
80
- #ar-button:active {
81
- background-color: #6d28d9;
82
  }
83
 
84
  #ar-prompt {
@@ -125,50 +132,198 @@
125
  0% { transform: rotate(0deg); }
126
  100% { transform: rotate(360deg); }
127
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  </style>
129
  </head>
130
  <body>
131
- <header>
132
  <span class="logo">23DFactory Mobile</span>
133
  <a href="/" class="btn-close">Ir al inicio</a>
134
  </header>
135
 
136
- <div id="loading-overlay">
137
- <div class="spinner"></div>
138
- <div style="font-size: 0.9rem; color: #a1a1aa;">Cargando modelo 3D...</div>
 
 
 
 
 
 
 
 
 
 
 
139
  </div>
140
 
141
- <model-viewer
142
- id="viewer"
143
- src=""
144
- ar
145
- ar-modes="webxr scene-viewer quick-look"
146
- camera-controls
147
- touch-action="pan-y"
148
- shadow-intensity="1"
149
- auto-rotate
150
- interaction-prompt="auto">
151
-
152
- <button slot="ar-button" id="ar-button">
153
- ⚡ Ver en Realidad Aumentada (AR)
154
- </button>
155
-
156
- <div id="ar-prompt">
157
- Mueve tu celular para detectar el suelo
158
  </div>
159
- </model-viewer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  <script>
162
- // Get model path from URL query parameter
163
  const urlParams = new URLSearchParams(window.location.search);
164
  const modelUrl = urlParams.get('model');
 
 
 
165
  const viewer = document.getElementById('viewer');
166
  const overlay = document.getElementById('loading-overlay');
 
 
 
 
167
 
168
- if (modelUrl) {
169
- viewer.src = modelUrl;
170
- } else {
171
- overlay.innerHTML = '<div style="color: #ef4444; text-align: center; padding: 24px;">No se especificó ningún modelo 3D.</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
174
  viewer.addEventListener('load', () => {
@@ -178,8 +333,67 @@
178
 
179
  viewer.addEventListener('error', (e) => {
180
  console.error('Error loading model:', e);
181
- overlay.innerHTML = '<div style="color: #ef4444; text-align: center; padding: 24px;">Error al cargar el archivo 3D.</div>';
182
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  </script>
184
  </body>
185
  </html>
 
32
  left: 0;
33
  right: 0;
34
  z-index: 10;
35
+ height: 48px;
36
+ box-sizing: border-box;
37
  }
38
 
39
  .logo {
 
54
  text-decoration: none;
55
  }
56
 
57
+ /* Container for viewer */
58
+ #viewer-container {
59
+ width: 100%;
60
+ height: 100%;
61
+ position: relative;
62
+ display: none;
63
+ }
64
+
65
  model-viewer {
66
  width: 100%;
67
  height: 100%;
 
72
  #ar-button {
73
  background-color: #7c3aed;
74
  color: white;
75
+ border-radius: 8px;
76
  border: none;
77
  position: absolute;
78
  bottom: 24px;
79
  right: 24px;
80
+ padding: 14px 20px;
81
  font-size: 0.95rem;
82
  font-weight: 600;
83
  cursor: pointer;
84
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4);
85
  display: flex;
86
  align-items: center;
87
  gap: 8px;
88
+ z-index: 99;
 
 
 
89
  }
90
 
91
  #ar-prompt {
 
132
  0% { transform: rotate(0deg); }
133
  100% { transform: rotate(360deg); }
134
  }
135
+
136
+ /* Gallery Selector Page */
137
+ #gallery-container {
138
+ flex: 1;
139
+ padding: 70px 20px 20px 20px;
140
+ overflow-y: auto;
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: 16px;
144
+ box-sizing: border-box;
145
+ }
146
+
147
+ .gallery-title {
148
+ font-size: 1.35rem;
149
+ font-weight: 700;
150
+ margin-bottom: 4px;
151
+ background: linear-gradient(135deg, #ffffff, #a1a1aa);
152
+ -webkit-background-clip: text;
153
+ -webkit-text-fill-color: transparent;
154
+ }
155
+
156
+ .gallery-desc {
157
+ font-size: 0.85rem;
158
+ color: #a1a1aa;
159
+ margin-bottom: 16px;
160
+ line-height: 1.4;
161
+ }
162
+
163
+ .gallery-grid {
164
+ display: grid;
165
+ grid-template-columns: 1fr;
166
+ gap: 16px;
167
+ }
168
+
169
+ .model-card {
170
+ background: rgba(255, 255, 255, 0.02);
171
+ border: 1px solid rgba(255, 255, 255, 0.05);
172
+ border-radius: 12px;
173
+ padding: 16px;
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: space-between;
177
+ gap: 12px;
178
+ transition: border-color 0.2s;
179
+ }
180
+
181
+ .model-card:active {
182
+ border-color: #7c3aed;
183
+ background: rgba(124, 58, 237, 0.05);
184
+ }
185
+
186
+ .model-info {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 12px;
190
+ }
191
+
192
+ .model-icon {
193
+ background: rgba(124, 58, 237, 0.1);
194
+ color: #c084fc;
195
+ width: 44px;
196
+ height: 44px;
197
+ border-radius: 10px;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ font-size: 1.3rem;
202
+ }
203
+
204
+ .model-name {
205
+ font-weight: 600;
206
+ font-size: 0.9rem;
207
+ color: #ffffff;
208
+ margin-bottom: 4px;
209
+ }
210
+
211
+ .model-date {
212
+ font-size: 0.75rem;
213
+ color: #a1a1aa;
214
+ }
215
+
216
+ .btn-view {
217
+ background: #7c3aed;
218
+ color: #ffffff;
219
+ border: none;
220
+ padding: 8px 16px;
221
+ border-radius: 8px;
222
+ font-size: 0.85rem;
223
+ font-weight: 600;
224
+ cursor: pointer;
225
+ }
226
+
227
+ .btn-back {
228
+ position: absolute;
229
+ top: 12px;
230
+ left: 16px;
231
+ background: rgba(255, 255, 255, 0.08);
232
+ border: none;
233
+ color: #ffffff;
234
+ padding: 6px 12px;
235
+ border-radius: 6px;
236
+ font-size: 0.8rem;
237
+ font-weight: 600;
238
+ z-index: 99;
239
+ cursor: pointer;
240
+ display: none;
241
+ }
242
  </style>
243
  </head>
244
  <body>
245
+ <header id="app-header">
246
  <span class="logo">23DFactory Mobile</span>
247
  <a href="/" class="btn-close">Ir al inicio</a>
248
  </header>
249
 
250
+ <button id="btnBackToGallery" class="btn-back">⬅ Volver</button>
251
+
252
+ <!-- 1. Gallery selector (shown if no model query param) -->
253
+ <div id="gallery-container">
254
+ <div>
255
+ <h2 class="gallery-title">Tu Biblioteca 3D</h2>
256
+ <p class="gallery-desc">Selecciona uno de tus modelos para visualizar en 3D interactivo y proyectar en tu habitación usando AR.</p>
257
+ </div>
258
+ <div class="gallery-grid" id="gallery-grid">
259
+ <div style="text-align: center; padding: 40px; color: #a1a1aa; font-size: 0.9rem;">
260
+ <div class="spinner" style="margin: 0 auto 16px auto;"></div>
261
+ Cargando tu galería...
262
+ </div>
263
+ </div>
264
  </div>
265
 
266
+ <!-- 2. Interactive model viewer (shown when active) -->
267
+ <div id="viewer-container">
268
+ <div id="loading-overlay">
269
+ <div class="spinner"></div>
270
+ <div id="loading-text" style="font-size: 0.9rem; color: #a1a1aa;">Cargando archivo 3D...</div>
 
 
 
 
 
 
 
 
 
 
 
 
271
  </div>
272
+
273
+ <model-viewer
274
+ id="viewer"
275
+ src=""
276
+ ar
277
+ ar-modes="webxr scene-viewer quick-look"
278
+ camera-controls
279
+ touch-action="pan-y"
280
+ shadow-intensity="1"
281
+ auto-rotate
282
+ interaction-prompt="auto">
283
+
284
+ <button slot="ar-button" id="ar-button">
285
+ ⚡ Proyectar en AR (Cámara)
286
+ </button>
287
+
288
+ <div id="ar-prompt">
289
+ Mueve tu celular para detectar el suelo
290
+ </div>
291
+ </model-viewer>
292
+ </div>
293
 
294
  <script>
 
295
  const urlParams = new URLSearchParams(window.location.search);
296
  const modelUrl = urlParams.get('model');
297
+
298
+ const galleryContainer = document.getElementById('gallery-container');
299
+ const viewerContainer = document.getElementById('viewer-container');
300
  const viewer = document.getElementById('viewer');
301
  const overlay = document.getElementById('loading-overlay');
302
+ const loadingText = document.getElementById('loading-text');
303
+ const btnBack = document.getElementById('btnBackToGallery');
304
+ const appHeader = document.getElementById('app-header');
305
+ const galleryGrid = document.getElementById('gallery-grid');
306
 
307
+ // Back button to list models
308
+ btnBack.addEventListener('click', () => {
309
+ viewerContainer.style.display = 'none';
310
+ btnBack.style.display = 'none';
311
+ appHeader.style.display = 'flex';
312
+ galleryContainer.style.display = 'flex';
313
+ viewer.src = '';
314
+ });
315
+
316
+ // Load 3D model in viewer
317
+ function loadModelInViewer(url) {
318
+ galleryContainer.style.display = 'none';
319
+ appHeader.style.display = 'none';
320
+ viewerContainer.style.display = 'block';
321
+ btnBack.style.display = 'block';
322
+
323
+ overlay.style.display = 'flex';
324
+ overlay.style.opacity = 1;
325
+ loadingText.textContent = "Cargando archivo 3D...";
326
+ viewer.src = url;
327
  }
328
 
329
  viewer.addEventListener('load', () => {
 
333
 
334
  viewer.addEventListener('error', (e) => {
335
  console.error('Error loading model:', e);
336
+ loadingText.innerHTML = '<span style="color: #ef4444;">Error al cargar el archivo 3D. Asegúrate de que el formato sea correcto.</span>';
337
  });
338
+
339
+ // Fetch user gallery from server
340
+ async function fetchGallery() {
341
+ try {
342
+ const response = await fetch('/api/gallery');
343
+ if (!response.ok) throw new Error('Failed to fetch gallery');
344
+ const data = await response.json();
345
+
346
+ // Filter only 3D models (GLB files)
347
+ const models = data.filter(item => item.type === '3d');
348
+
349
+ if (models.length === 0) {
350
+ galleryGrid.innerHTML = `
351
+ <div style="text-align: center; padding: 40px; color: #a1a1aa; border: 1px dashed rgba(255,255,255,0.08); border-radius: 12px;">
352
+ <div style="font-size: 2.5rem; margin-bottom: 12px;">📦</div>
353
+ <div style="font-size: 0.9rem; font-weight: 600; color: #ffffff; margin-bottom: 4px;">No tienes modelos guardados</div>
354
+ <div style="font-size: 0.8rem; color: #a1a1aa;">Crea un modelo 3D desde tu PC para verlo aquí.</div>
355
+ </div>
356
+ `;
357
+ return;
358
+ }
359
+
360
+ galleryGrid.innerHTML = '';
361
+ models.forEach(model => {
362
+ const date = new Date(model.timestamp * 1000).toLocaleString('es-ES', {
363
+ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
364
+ });
365
+
366
+ const card = document.createElement('div');
367
+ card.className = 'model-card';
368
+ card.innerHTML = `
369
+ <div class="model-info">
370
+ <div class="model-icon">📦</div>
371
+ <div>
372
+ <div class="model-name">${model.filename.split('/').pop()}</div>
373
+ <div class="model-date">${date}</div>
374
+ </div>
375
+ </div>
376
+ <button class="btn-view">Ver 3D</button>
377
+ `;
378
+
379
+ card.querySelector('.btn-view').addEventListener('click', () => {
380
+ loadModelInViewer(model.downloadUrl);
381
+ });
382
+
383
+ galleryGrid.appendChild(card);
384
+ });
385
+ } catch (err) {
386
+ console.error(err);
387
+ galleryGrid.innerHTML = `<div style="color: #ef4444; text-align: center; padding: 24px;">Error al conectar con la galería del servidor. Por favor, inicia sesión.</div>`;
388
+ }
389
+ }
390
+
391
+ // Routing Logic
392
+ if (modelUrl) {
393
+ loadModelInViewer(modelUrl);
394
+ } else {
395
+ fetchGallery();
396
+ }
397
  </script>
398
  </body>
399
  </html>