From 43770c6d676faac7ae4bc0ca65746a06778e998a Mon Sep 17 00:00:00 2001 From: julio Date: Thu, 8 Oct 2026 03:17:28 -0300 Subject: [PATCH] =?UTF-8?q?feat(celular):=20sem=20a=20placa=20a=20carinha?= =?UTF-8?q?=20dorme=20e=20mostra=20placa=20=E2=9C=95=20celular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- android/app/AndroidManifest.xml | 21 ++- android/app/assets/carinha.html | 167 ++++++++++++++++-- android/app/java/br/carinha/rosto/Admin.java | 10 ++ .../java/br/carinha/rosto/MainActivity.java | 124 ++++++++++++- .../app/java/br/carinha/rosto/Quiosque.java | 118 +++++++++++++ android/app/res/xml/admin.xml | 5 + android/enxugar-s10.ps1 | 37 ++++ carinha.html | 167 ++++++++++++++++-- 8 files changed, 620 insertions(+), 29 deletions(-) create mode 100644 android/app/java/br/carinha/rosto/Admin.java create mode 100644 android/app/java/br/carinha/rosto/Quiosque.java create mode 100644 android/app/res/xml/admin.xml create mode 100644 android/enxugar-s10.ps1 diff --git a/android/app/AndroidManifest.xml b/android/app/AndroidManifest.xml index 8a1e9cf..0c5b39f 100644 --- a/android/app/AndroidManifest.xml +++ b/android/app/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="12" + android:versionName="0.9.0"> @@ -36,6 +36,23 @@ + + + + + + + + + + + + + + diff --git a/android/app/assets/carinha.html b/android/app/assets/carinha.html index 0ab791e..74e69ac 100644 --- a/android/app/assets/carinha.html +++ b/android/app/assets/carinha.html @@ -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}} @@ -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) => + '' + + '' + + '' + + [0,1,2,3,4,5,6,7].map((i) => '').join("") + + '' + + '' + + ''; + const celular = (cx, cy) => + '' + + '' + + '' + + '' + + ''; + const xis = (x, y) => ''; + el.innerHTML = + '' + + '' + + '
sem conexão com a placa
' + + '
'; + 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); +