shell.html (7889B)
1 <!doctype html> 2 <!-- shell.html - pagina de la version web de Catan (emscripten la completa con el 3 script del modulo en {{{ SCRIPT }}}). El canvas llena la pantalla; la app en C 4 decide el layout (ancho o vertical) segun la forma que le queda. --> 5 <html lang="es"> 6 <head> 7 <meta charset="utf-8"> 8 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no"> 9 <meta name="theme-color" content="#1c5696"> 10 <meta name="apple-mobile-web-app-capable" content="yes"> 11 <meta name="mobile-web-app-capable" content="yes"> 12 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> 13 <meta name="apple-mobile-web-app-title" content="Catan"> 14 <link rel="manifest" href="manifest.json"> 15 <link rel="icon" href="icon-192.png"> 16 <link rel="apple-touch-icon" href="icon-192.png"> 17 <title>Catan</title> 18 <style> 19 html, body { margin: 0; padding: 0; height: 100%; background: #1c5696; overflow: hidden; overscroll-behavior: none; } 20 body { font-family: system-ui, sans-serif; color: #e6e6e6; } 21 #canvas { display: block; width: 100vw; height: 100dvh; border: 0; padding: 0; margin: 0; touch-action: none; 22 image-rendering: pixelated; outline: none; } 23 #status { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; 24 flex-direction: column; gap: 12px; background: #1c5696; z-index: 5; } 25 #status.hidden { display: none; } 26 #status p { margin: 0; opacity: .8; } 27 /* entrada de texto (chat, nombre, contrasena): el navegador no muestra el teclado 28 tactil para el canvas, asi que se usa un campo HTML encima */ 29 #gk-input { position: fixed; left: 0; right: 0; top: 0; padding: 10px; background: #222630; z-index: 1000; 30 display: none; box-shadow: 0 2px 8px rgba(0,0,0,.5); } 31 #gk-input.show { display: block; } 32 #gk-input label { display: block; font-size: 14px; margin-bottom: 6px; color: #ffc83c; } 33 #gk-input .row { display: flex; gap: 8px; } 34 #gk-input input { flex: 1; font-size: 18px; padding: 8px; border: 1px solid #8c92a0; border-radius: 4px; 35 background: #303644; color: #fff; min-width: 0; } 36 #gk-input button { font-size: 16px; padding: 8px 14px; border: 0; border-radius: 4px; background: #ffc83c; color: #181410; } 37 #gk-input button.cancel { background: #8c92a0; } 38 </style> 39 </head> 40 <body> 41 <div id="status"><p id="status-text">Cargando Catan…</p></div> 42 <div id="gk-input"> 43 <label id="gk-label" for="gk-field"></label> 44 <div class="row"> 45 <input id="gk-field" type="text" autocomplete="off" autocapitalize="off" enterkeyhint="done" placeholder="tocá acá para escribir"> 46 <button id="gk-ok">OK</button> 47 <button id="gk-cancel" class="cancel">✕</button> 48 </div> 49 </div> 50 <canvas id="canvas" tabindex="-1" oncontextmenu="event.preventDefault()"></canvas> 51 <script> 52 // Si otra pestana tiene el candado, no se arranca el modulo (ver mas abajo) 53 var _origLoad = null; 54 // ---- entrada de texto para la app en C (gk_text_input en comun/linux/guikit.c) 55 var gkText = { done: 0, value: '' }; 56 var gkBox = document.getElementById('gk-input'), gkField = document.getElementById('gk-field'); 57 function gkTextOpen(title, init, max) { 58 // SDL (FILL_DOCUMENT) esconde en un div oculto todo lo que habia en el body al crear 59 // la ventana: el cuadro se vuelve a colgar del body para que se vea encima del canvas 60 if (gkBox.parentNode !== document.body) document.body.appendChild(gkBox); 61 document.getElementById('gk-label').textContent = title; 62 gkField.value = init; 63 gkField.maxLength = max > 0 ? max : 200; 64 gkField.type = /contrase|password/i.test(title) ? 'password' : 'text'; 65 gkText.done = 0; 66 gkBox.classList.add('show'); 67 gkField.focus(); 68 gkField.select(); 69 setTimeout(function () { if (gkText.done === 0) gkField.focus(); }, 50); // por si el canvas se lo queda 70 } 71 // mientras el cuadro esta abierto, el foco vuelve al campo si algo se lo lleva 72 gkField.addEventListener('focusout', function () { 73 if (gkText.done === 0 && gkBox.classList.contains('show')) setTimeout(function () { gkField.focus(); }, 0); 74 }); 75 function gkTextClose(ok) { 76 gkText.value = gkField.value; 77 gkText.done = ok ? 1 : -1; 78 gkBox.classList.remove('show'); 79 gkField.blur(); 80 } 81 function gkTextDone() { return gkText.done; } 82 function gkTextValue() { return gkText.value; } 83 document.getElementById('gk-ok').onclick = function () { gkTextClose(true); }; 84 document.getElementById('gk-cancel').onclick = function () { gkTextClose(false); }; 85 gkField.addEventListener('keydown', function (e) { 86 if (e.key === 'Enter') { e.preventDefault(); gkTextClose(true); } 87 else if (e.key === 'Escape') { e.preventDefault(); gkTextClose(false); } 88 e.stopPropagation(); // que SDL no vea estas teclas 89 }); 90 gkField.addEventListener('keyup', function (e) { e.stopPropagation(); }); 91 gkField.addEventListener('keypress', function (e) { e.stopPropagation(); }); 92 93 // ---- el modulo emscripten 94 var wsParam = new URLSearchParams(location.search).get('ws'); 95 var Module = { 96 noStart: false, 97 // el candado de instancia se pide arriba de forma asincronica: antes de correr 98 // main se espera a saber si lo tenemos 99 preInit: [function () { 100 addRunDependency('lock'); 101 var tries = 0; 102 (function wait() { 103 if (Module.noStart) return; // nunca se libera: la pagina queda en el aviso 104 if (Module.lockDone || !(navigator.locks && navigator.locks.request) || ++tries > 50) removeRunDependency('lock'); 105 else setTimeout(wait, 20); 106 })(); 107 }], 108 canvas: document.getElementById('canvas'), 109 // los sockets BSD del programa van por este WebSocket: /ws de la misma pagina 110 // (Caddy lo pasa al catan-server), o ?ws=ws://localhost:7373 para probar local 111 websocket: { 112 url: wsParam || ((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws'), 113 subprotocol: 'binary' 114 }, 115 setStatus: function (t) { 116 var el = document.getElementById('status-text'); 117 if (el) el.textContent = t || ''; 118 }, 119 onRuntimeInitialized: function () { document.getElementById('status').classList.add('hidden'); }, 120 onAbort: function (what) { 121 var st = document.getElementById('status'); 122 if (st.parentNode !== document.body) document.body.appendChild(st); 123 Module.setStatus('Error: ' + what); 124 st.classList.remove('hidden'); 125 }, 126 printErr: function (t) { console.warn(t); } 127 }; 128 if ('serviceWorker' in navigator && location.protocol === 'https:') { 129 // la app sale de la cache (cache-first); cuando una version nueva toma el control, 130 // recargar si recien se abrio (todavia en el menu), para no cortar una partida. 131 // En la primera visita no habia controlador: ahi el SW nuevo no es una actualizacion. 132 var hadSW = !!navigator.serviceWorker.controller; 133 navigator.serviceWorker.register('sw.js').catch(function (e) { console.warn('sw', e); }); 134 navigator.serviceWorker.addEventListener('controllerchange', function () { 135 if (hadSW && performance.now() < 15000) location.reload(); 136 }); 137 } 138 // Una sola instancia: dos pestanas (o la app instalada y una pestana) tienen cada una 139 // su copia de /data y la que guarda ultima pisa a la otra. Con Web Locks la segunda 140 // no arranca; en navegadores sin locks, arranca igual. 141 if (navigator.locks && navigator.locks.request) { 142 navigator.locks.request('catan', { ifAvailable: true }, function (lock) { 143 if (lock) { Module.lockDone = true; return new Promise(function () {}); } // se tiene mientras viva la pagina 144 document.getElementById('status-text').textContent = 'Catan ya está abierto en otra pestaña o ventana. Cerrá esa y recargá esta.'; 145 Module.noStart = true; 146 return null; 147 }); 148 } 149 </script> 150 {{{ SCRIPT }}} 151 </body> 152 </html>