feat(celular): sem a placa a carinha dorme e mostra placa ✕ celular
Sem servidor o app não finge mais que está tudo normal: - o app guarda a última aparência e os ajustes vindos da placa (ponte CarinhaApp); a cópia do APK abre com eles; - a carinha dorme e não acorda com nada; no canto direito aparece a placa ✕ celular animada com "sem conexão com a placa"; - o app tenta a placa a cada 10 s e volta sozinho; se a placa cai com a página aberta, em 15 s ela dorme e mostra o aviso; - endereço padrão passa a ser a Orange Pi (192.168.1.35:8100). Também no app (0.9.0): - modo quiosque como dono do aparelho (Quiosque/Admin, menu "Modo quiosque"); a Samsung recusa com bootloader desbloqueado, vale no LineageOS; - carinha como tela inicial (HOME); - android/enxugar-s10.ps1 desativa 57 apps sem uso no S10 (-Reativar desfaz). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+156
-11
@@ -20,6 +20,52 @@
|
||||
@keyframes aparecerFigura{from{opacity:0}to{opacity:1}}
|
||||
@media(prefers-reduced-motion:reduce){.iconeOlho,.figurinhaDinamica{animation:none}}
|
||||
#fps{position:fixed;left:4px;top:2px;color:#2b3a4a;font:12px monospace;display:none}
|
||||
/* sem a placa: placa ✕ celular na cor dos olhos. Deitado, o rosto dormindo fica onde está e o
|
||||
desenho fica em pé no canto direito; em pé, o rosto sobe e o desenho vai embaixo. */
|
||||
#semServidor{position:fixed;right:2.5vw;top:50%;transform:translateY(-50%);width:min(24vw,42vh);
|
||||
z-index:5;pointer-events:none;text-align:center;opacity:.82;font-family:system-ui,sans-serif;
|
||||
animation:semServidorEntra 1.2s ease-out}
|
||||
#semServidor svg{display:block;margin:0 auto;overflow:visible;filter:drop-shadow(0 0 3px currentColor)}
|
||||
#semServidor svg.horizontal{display:none}
|
||||
#semServidor svg.vertical{height:58vh;width:auto;max-width:100%}
|
||||
#semServidor svg.vertical .olhinhos{animation-name:semServidorOlharCima}
|
||||
@media (orientation:portrait){
|
||||
body.semServidor #tela,body.semServidor #rostoMotor{transform:translateY(-20%) scale(.8)}
|
||||
#semServidor{right:auto;top:auto;left:50%;bottom:3.5vh;transform:translateX(-50%);width:min(80vw,400px)}
|
||||
#semServidor svg.horizontal{display:block;width:100%;height:auto}
|
||||
#semServidor svg.vertical{display:none}
|
||||
}
|
||||
#semServidor .titulo{text-wrap:balance;font-weight:600;font-size:clamp(12px,3.2vh,20px);margin-top:.4em;letter-spacing:.02em}
|
||||
#semServidor .sub{font-size:clamp(10px,2.4vh,15px);opacity:.6;margin-top:.25em;font-variant-numeric:tabular-nums}
|
||||
#semServidor .placa rect,#semServidor .celular>rect{fill:none;stroke:currentColor;stroke-width:3}
|
||||
#semServidor .chip{fill:currentColor;fill-opacity:.25}
|
||||
#semServidor .pino{fill:currentColor;opacity:.7}
|
||||
#semServidor .led{fill:#ff5a5a;animation:semServidorLed 1.6s steps(1) infinite}
|
||||
#semServidor .cabo{stroke:currentColor;stroke-width:3;stroke-dasharray:5 6;opacity:.5;fill:none}
|
||||
#semServidor .arco{fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;opacity:0;
|
||||
animation:semServidorArco 2.4s ease-out infinite}
|
||||
#semServidor .a2{animation-delay:.3s}
|
||||
#semServidor .a3{animation-delay:.6s}
|
||||
#semServidor .xis path{stroke:#ff5a5a;stroke-width:5;stroke-linecap:round;fill:none}
|
||||
#semServidor .xis{transform-box:fill-box;transform-origin:center;animation:semServidorXis 2.4s ease-in-out infinite}
|
||||
#semServidor .olhinhos{fill:currentColor;transform-box:fill-box;transform-origin:center;
|
||||
animation:semServidorOlhar 5s ease-in-out infinite}
|
||||
#semServidor .boquinha{fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round}
|
||||
#semServidor .celular{transform-box:fill-box;transform-origin:50% 100%;animation:semServidorProcura 4.8s ease-in-out infinite}
|
||||
#semServidor.cutucado .celular{animation:semServidorChacoalha .5s ease-in-out}
|
||||
@keyframes semServidorEntra{from{opacity:0}to{opacity:.82}}
|
||||
/* em pé no canto direito: o celularzinho olha para cima, para a placa */
|
||||
@keyframes semServidorOlharCima{0%,30%{transform:translateY(-3px) scaleY(1)}34%{transform:translateY(-3px) scaleY(.1)}38%,60%{transform:translateY(-3px) scaleY(1)}
|
||||
70%,90%{transform:translateY(1px) scaleY(1)}100%{transform:translateY(-3px)}}
|
||||
@keyframes semServidorArco{0%{opacity:0}25%{opacity:.9}70%,100%{opacity:0}}
|
||||
@keyframes semServidorXis{0%,55%,100%{transform:scale(1);opacity:.75}65%{transform:scale(1.25);opacity:1}80%{transform:scale(1);opacity:.85}}
|
||||
@keyframes semServidorLed{0%{opacity:1}50%{opacity:.15}}
|
||||
/* o celularzinho olha para a placa, olha de volta e pisca */
|
||||
@keyframes semServidorOlhar{0%,30%{transform:translateX(-3px) scaleY(1)}34%{transform:translateX(-3px) scaleY(.1)}38%,60%{transform:translateX(-3px) scaleY(1)}
|
||||
70%,90%{transform:translateX(1px) scaleY(1)}100%{transform:translateX(-3px)}}
|
||||
@keyframes semServidorProcura{0%,100%{transform:rotate(0)}20%{transform:rotate(-5deg)}40%{transform:rotate(0)}}
|
||||
@keyframes semServidorChacoalha{0%,100%{transform:rotate(0)}25%{transform:rotate(-9deg)}50%{transform:rotate(7deg)}75%{transform:rotate(-4deg)}}
|
||||
@media(prefers-reduced-motion:reduce){#semServidor *,#semServidor{animation:none!important}#semServidor .arco{opacity:.6}}
|
||||
</style>
|
||||
<body>
|
||||
<svg id="tela" viewBox="0 0 480 320" preserveAspectRatio="xMidYMid meet">
|
||||
@@ -106,7 +152,7 @@ function cuidarDaTela() {
|
||||
const agora = performance.now();
|
||||
if (!r.dormindo) SONO.desde = 0;
|
||||
else if (!SONO.desde) SONO.desde = agora;
|
||||
const apagar = SONO.apagar && r.dormindo && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
||||
const apagar = SONO.apagar && r.dormindo && !window.carinhaSemServidor && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
||||
if (apagar !== SONO.apagada) {
|
||||
SONO.apagada = apagar;
|
||||
window.carinhaTelaApagada = apagar;
|
||||
@@ -725,6 +771,11 @@ function trata(ordem) {
|
||||
r.dormindo = true;
|
||||
atividade(null);
|
||||
} else if (ordem === "acordar") {
|
||||
if (window.carinhaSemServidor) { // sem a placa continua dormindo; o aviso chacoalha
|
||||
const aviso = document.getElementById("semServidor");
|
||||
if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); }
|
||||
return;
|
||||
}
|
||||
r.dormindo = false; animar("surpreso");
|
||||
} else if (ordem === "aleatorio") {
|
||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||
@@ -777,27 +828,44 @@ async function buscarOrdens() {
|
||||
try {
|
||||
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
conexaoOk();
|
||||
const lista = await resp.json();
|
||||
for (const nome of lista) trata(nome);
|
||||
} catch (e) { /* sem servidor (file://) — ignora */ }
|
||||
}
|
||||
setInterval(buscarOrdens, 500);
|
||||
|
||||
// O app (window.CarinhaApp) guarda a ultima aparencia e os ajustes vindos do servidor:
|
||||
// a copia do APK (file://) nao enxerga o armazenamento da pagina do servidor.
|
||||
const LOCAL = location.protocol === "file:";
|
||||
function lembrar(chave, valor) {
|
||||
try { if (window.CarinhaApp && !LOCAL) window.CarinhaApp.guardar(chave, JSON.stringify(valor)); } catch (e) {}
|
||||
}
|
||||
function lembrado(chave) {
|
||||
try { const s = window.CarinhaApp && window.CarinhaApp.lembrado(chave); return s ? JSON.parse(s) : null; } catch (e) { return null; }
|
||||
}
|
||||
|
||||
// A aparencia guardada no servidor (rosto + cor) e aplicada ao carregar: o
|
||||
// painel de controle personaliza e a carinha ja abre do jeito escolhido. Sem
|
||||
// servidor (file://) isso falha calado e vale o padrao.
|
||||
// servidor vale a ultima que o app guardou (ou o padrao).
|
||||
async function aplicarAparencia() {
|
||||
try {
|
||||
const resp = await fetch("config", { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
const c = await resp.json();
|
||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||
} catch (e) { /* sem servidor — segue com o padrao */ }
|
||||
let c = null;
|
||||
if (!LOCAL) {
|
||||
try {
|
||||
const resp = await fetch("config", { cache: "no-store" });
|
||||
if (resp.ok) c = await resp.json();
|
||||
} catch (e) { /* sem servidor */ }
|
||||
}
|
||||
if (!c) c = lembrado("aparencia");
|
||||
if (!c) return;
|
||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||
}
|
||||
setTimeout(aplicarAparencia, 200);
|
||||
// O painel troca rosto/cor por ordens: guarda a aparencia atual de tempos em tempos.
|
||||
setInterval(() => { if (!window.carinhaSemServidor) lembrar("aparencia", { rosto: ROSTO_NOME, formato: FORMATO, cor: CIANO.slice() }); }, 15000);
|
||||
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
||||
// do servidor (SSE) por postMessage.
|
||||
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
||||
@@ -1238,6 +1306,7 @@ function configurarApp(c) {
|
||||
MOV.cooldown = c.movcooldown; MOV.olharMax = c.movolhar;
|
||||
if (!c.reagir) { MOV.olharX = MOV.olharY = 0; MOV.ultimoMov = 0; }
|
||||
configRecebida = c;
|
||||
lembrar('config', c);
|
||||
if(!c.seguirRastreamento || !c.sensorCamera || !c.reagir) TRACK.alvo=null;
|
||||
}
|
||||
let sincronizando = false;
|
||||
@@ -1363,6 +1432,7 @@ function laco(t) {
|
||||
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
|
||||
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
|
||||
|
||||
if (window.carinhaSemServidor && !r.dormindo) { r.fila = []; r.kf = null; r.dormindo = true; } // sem a placa: só dorme
|
||||
const ocupado = congelado ? true : passo(dt);
|
||||
if (!ocupado) {
|
||||
descanso += dt;
|
||||
@@ -1434,6 +1504,81 @@ setTimeout(relatar, 1500);
|
||||
setInterval(poll,1000);poll();
|
||||
})();
|
||||
|
||||
/* ---------------------------------------------------------------- sem servidor
|
||||
Sem a placa (copia do APK, ou o servidor caiu com a pagina aberta) a carinha dorme com a
|
||||
ultima aparencia e ajustes guardados e mostra placa ✕ celular. Volta sozinha quando a placa
|
||||
responde (o app recarrega a pagina do servidor; aberta pelo servidor, o SSE reconecta). */
|
||||
let ultimoOk = performance.now();
|
||||
const TOLERANCIA_SEM_SERVIDOR = 15000; // ms sem resposta antes de dormir
|
||||
function conexaoOk() { ultimoOk = performance.now(); }
|
||||
|
||||
function mostrarSemServidor(ligar) {
|
||||
let el = document.getElementById("semServidor");
|
||||
document.body.classList.toggle("semServidor", ligar);
|
||||
if (!ligar) { if (el) el.hidden = true; return; }
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "semServidor";
|
||||
// placa e celular pelo canto de cima à esquerda; o desenho vertical vai no canto direito com o celular deitado
|
||||
const placa = (px, py) =>
|
||||
'<g class="placa">' +
|
||||
'<rect x="' + px + '" y="' + py + '" width="84" height="56" rx="6"/>' +
|
||||
'<rect class="chip" x="' + (px + 29) + '" y="' + (py + 15) + '" width="26" height="26" rx="3"/>' +
|
||||
[0,1,2,3,4,5,6,7].map((i) => '<circle class="pino" cx="' + (px + 11 + i * 9) + '" cy="' + (py + 7) + '" r="2"/>').join("") +
|
||||
'<circle class="pino" cx="' + (px + 7) + '" cy="' + (py + 50) + '" r="2.4"/><circle class="pino" cx="' + (px + 77) + '" cy="' + (py + 50) + '" r="2.4"/>' +
|
||||
'<circle class="led" cx="' + (px + 72) + '" cy="' + (py + 22) + '" r="2.6"/>' +
|
||||
'</g>';
|
||||
const celular = (cx, cy) =>
|
||||
'<g class="celular">' +
|
||||
'<rect x="' + cx + '" y="' + cy + '" width="40" height="76" rx="8"/>' +
|
||||
'<g class="olhinhos"><ellipse cx="' + (cx + 13) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/><ellipse cx="' + (cx + 27) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/></g>' +
|
||||
'<path class="boquinha" d="M' + (cx + 14) + ' ' + (cy + 48) + ' Q' + (cx + 20) + ' ' + (cy + 44) + ' ' + (cx + 26) + ' ' + (cy + 48) + '"/>' +
|
||||
'</g>';
|
||||
const xis = (x, y) => '<g class="xis"><path d="M' + (x - 10) + ' ' + (y - 10) + ' L' + (x + 10) + ' ' + (y + 10) + ' M' + (x + 10) + ' ' + (y - 10) + ' L' + (x - 10) + ' ' + (y + 10) + '"/></g>';
|
||||
el.innerHTML =
|
||||
'<svg class="horizontal" viewBox="0 0 300 104" aria-hidden="true">' + placa(18, 30) +
|
||||
'<path class="cabo" d="M104 58 H146"/>' +
|
||||
'<g class="arcos">' +
|
||||
'<path class="arco a1" d="M212 50 A10 10 0 0 0 212 66"/>' +
|
||||
'<path class="arco a2" d="M203 44 A18 18 0 0 0 203 72"/>' +
|
||||
'<path class="arco a3" d="M194 38 A26 26 0 0 0 194 78"/>' +
|
||||
'</g>' + xis(160, 58) + celular(220, 18) +
|
||||
'</svg>' +
|
||||
'<svg class="vertical" viewBox="0 0 120 262" aria-hidden="true">' + placa(18, 8) +
|
||||
'<path class="cabo" d="M60 66 V108"/>' +
|
||||
'<g class="arcos">' +
|
||||
'<path class="arco a1" d="M52 172 A10 10 0 0 1 68 172"/>' +
|
||||
'<path class="arco a2" d="M46 163 A18 18 0 0 1 74 163"/>' +
|
||||
'<path class="arco a3" d="M40 154 A26 26 0 0 1 80 154"/>' +
|
||||
'</g>' + xis(60, 122) + celular(40, 182) +
|
||||
'</svg>' +
|
||||
'<div class="titulo">sem conexão com a placa</div>' +
|
||||
'<div class="sub"></div>';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
let alvo = "";
|
||||
try { alvo = (window.CarinhaApp && window.CarinhaApp.servidor()) || (LOCAL ? "" : location.host); } catch (e) {}
|
||||
el.querySelector(".sub").textContent = "tentando de novo" + (alvo ? " · " + String(alvo).replace(/^https?:\/\//, "") : "") + "…";
|
||||
el.style.color = "rgb(" + CIANO.map(Math.round).join(",") + ")";
|
||||
el.hidden = false;
|
||||
}
|
||||
|
||||
function checarServidor() {
|
||||
if (MODO_PAINEL || par.get("espelho") === "1") return;
|
||||
if (window.carinhaSSE) conexaoOk();
|
||||
const fora = LOCAL || performance.now() - ultimoOk > TOLERANCIA_SEM_SERVIDOR;
|
||||
if (window.carinhaSemServidor) mostrarSemServidor(true); // acompanha a cor guardada
|
||||
if (fora === !!window.carinhaSemServidor) return;
|
||||
window.carinhaSemServidor = fora;
|
||||
console.log(fora ? "sem servidor: dormindo" : "servidor de volta");
|
||||
mostrarSemServidor(fora);
|
||||
if (fora) { atividade(null); r.fila = []; r.kf = null; r.dormindo = true; }
|
||||
else trata("acordar");
|
||||
}
|
||||
// Copia do APK: os ultimos ajustes guardados (boca, sobrancelhas, sono...) valem também aqui.
|
||||
setTimeout(() => { if (LOCAL) { const c = lembrado("config"); if (c) configurarApp(c); } checarServidor(); }, 300);
|
||||
setInterval(checarServidor, 1000);
|
||||
|
||||
</script>
|
||||
<script src="/reacoes-render.js"></script>
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user