commit d95a3d80867be5604e642b69ecccd411eb700e21 Author: penguinehis Date: Sun Apr 26 12:51:15 2026 -0300 launch diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..67cdb09 --- /dev/null +++ b/readme.md @@ -0,0 +1,1140 @@ +## 1. Modelo de nomes do bridge + +O app expõe três estilos de bridge: + +1. Objetos nativos `Cake*`. São os objetos reais registrados pelo Android com `addJavascriptInterface`. +2. Aliases `Dt*`. Existem para compatibilidade com temas já existentes. +3. Helpers `CakeApp.*`. São pequenos wrappers JavaScript injetados pelo app. + +Para novos temas, você pode usar qualquer estilo, mas a abordagem recomendada é: + +- Use `Dt*` quando quiser máxima compatibilidade com temas existentes. +- Use `CakeApp.*` quando quiser código mais limpo. +- Sempre proteja as chamadas, porque o tema também pode ser aberto no navegador durante testes. + +Exemplo de helper seguro: + +```js +function bridgeExec(name, fallback = '') { + try { + const obj = window[name]; + if (obj && typeof obj.execute === 'function') return obj.execute(); + } catch (e) {} + return fallback; +} + +function bridgeCommand(name) { + try { + const obj = window[name]; + if (obj && typeof obj.execute === 'function') obj.execute(); + } catch (e) {} +} + +function bridgeSet(name, value) { + try { + const obj = window[name]; + if (obj && typeof obj.set === 'function') obj.set(String(value ?? '')); + } catch (e) {} +} + +function bridgeGet(name, fallback = '') { + try { + const obj = window[name]; + if (obj && typeof obj.get === 'function') return obj.get(); + } catch (e) {} + return fallback; +} +``` + +--- + +## 2. Comportamento importante do WebView + +O app ativa JavaScript e DOM storage no WebView do OnlineConfig. O HTML é carregado usando a URL do painel como URL base, então URLs relativas são resolvidas a partir do domínio do painel. + +Antes de carregar o HTML, o app sanitiza o layout: + +- Corrige alguns padrões quebrados de regex envolvendo quebras de linha. +- Remove qualquer bloco de script que comece neste marcador: + +```js +// --- AMBIENTE MOCK PARA TESTES --- +``` + +Não coloque código de produção abaixo desse marcador. O app trata isso como uma seção local de mock/teste e remove essa parte do tema carregado. + +O app injeta o bridge de compatibilidade antes de `` quando o HTML contém uma tag `head`. Se o HTML não tiver ``, o bridge é colocado no começo do documento. + +--- + +## 3. Esqueleto rápido de tema + +```html + + + + + Tema OnlineConfig + + + + +
+ +
+ +
+ +
+ + + + +
Desconectado
+ + + + +``` + +--- + +## 4. Helpers principais `CakeApp` + +O app injeta esse objeto para facilitar o código dos temas. + +### `CakeApp.vpn` + +```js +CakeApp.vpn.getState(); // retorna string +CakeApp.vpn.start(); // inicia/dispara o comando da VPN +CakeApp.vpn.stop(); // para a VPN +``` + +Aliases equivalentes: + +```js +DtGetVpnState.execute(); +DtExecuteVpnStart.execute(); +DtExecuteVpnStop.execute(); +``` + +### `CakeApp.configs` + +```js +CakeApp.configs.list(); // retorna string JSON com grupos/items +CakeApp.configs.current(); // retorna string JSON do perfil selecionado +CakeApp.configs.select(id); // seleciona um perfil pelo id +``` + +Aliases equivalentes: + +```js +DtGetConfigs.execute(); +DtGetDefaultConfig.execute(); +DtSetConfig.execute(id); +``` + +### `CakeApp.auth` + +```js +CakeApp.auth.getUsername(); +CakeApp.auth.setUsername(value); +CakeApp.auth.getPassword(); +CakeApp.auth.setPassword(value); +CakeApp.auth.getUuid(); +CakeApp.auth.setUuid(value); +``` + +Aliases equivalentes: + +```js +DtUsername.get(); +DtUsername.set(value); +DtPassword.get(); +DtPassword.set(value); +DtUuid.get(); +DtUuid.set(value); +``` + +### `CakeApp.system` + +```js +CakeApp.system.openUrl(url); +CakeApp.system.getStatusBarHeight(); +CakeApp.system.getNavigationBarHeight(); +``` + +Aliases equivalentes: + +```js +DtStartWebViewActivity.execute(url); +DtGetStatusBarHeight.execute(); +DtGetNavigationBarHeight.execute(); +``` + +--- + +## 5. Tabela completa das APIs do bridge + +### 5.1 APIs de config e perfil + +| API | Assinatura | Retorno | Finalidade | +|---|---:|---|---| +| `DtGetConfigs.execute()` | sem argumentos | string JSON | Retorna todos os grupos e perfis do OnlineConfig para montar a lista de servidores. | +| `DtGetDefaultConfig.execute()` | sem argumentos | string JSON | Retorna o perfil atualmente selecionado. Retorna `{}` se nada estiver selecionado. | +| `DtSetConfig.execute(id)` | string id | void | Seleciona um perfil/servidor pelo id e aplica nativamente. | +| `DtStartAppUpdate.execute()` | sem argumentos | void | Atualiza o catálogo/layout/cache de configs a partir do painel. | +| `DtStartUpdatePayload.execute()` | sem argumentos | void | Mesmo comportamento de `DtStartAppUpdate` nesta versão do app. | +| `DtGetLocalConfigVersion.execute()` | sem argumentos | string numérica | Retorna o último valor de timestamp/sincronização do OnlineConfig. Útil como marcador de versão. | + +### 5.2 APIs de autenticação + +| API | Assinatura | Retorno | Finalidade | +|---|---:|---|---| +| `DtUsername.get()` | sem argumentos | string | Retorna o usuário manual salvo, ou `Locked` quando a config selecionada não precisa de credenciais SSH manuais. | +| `DtUsername.set(value)` | string | void | Salva o usuário somente se credenciais manuais forem necessárias. Ignorado se estiver bloqueado. | +| `DtPassword.get()` | sem argumentos | string | Retorna a senha manual salva, ou `Locked` quando não for necessária. | +| `DtPassword.set(value)` | string | void | Salva a senha somente se credenciais manuais forem necessárias. Ignorado se estiver bloqueado. | +| `DtUuid.get()` | sem argumentos | string | Retorna o UUID manual V2Ray/Xray salvo, ou `Locked` quando a config selecionada não precisa de UUID manual. | +| `DtUuid.set(value)` | string | void | Salva o UUID. Nesta versão do app, escreve o valor do UUID e mantém cache para futuras seleções de config. | + +Importante: limpe `Locked` antes de colocar valores em inputs. + +```js +function cleanBridgeValue(value) { + const text = String(value ?? '').trim(); + return text.toLowerCase() === 'locked' ? '' : text; +} +``` + +### 5.3 APIs de controle e estado da VPN + +| API | Assinatura | Retorno | Finalidade | +|---|---:|---|---| +| `DtGetVpnState.execute()` | sem argumentos | string de estado | Lê o estado atual da VPN mapeado para o HTML. | +| `DtExecuteVpnStart.execute()` | sem argumentos | void | Inicia/dispara o comando nativo da VPN. | +| `DtExecuteVpnStop.execute()` | sem argumentos | void | Para a VPN. | +| `DtGetNetworkDownloadBytes.execute()` | sem argumentos | string numérica | Retorna bytes baixados atuais da rede/app para verificação de tráfego. | +| `DtGetLogs.execute()` | sem argumentos | string JSON | Retorna logs recentes de status/banner como array de objetos com timestamp. | +| `DtShowLoggerDialog.execute()` | sem argumentos | void | Abre o diálogo nativo de logs. | + +Possíveis estados de `DtGetVpnState` / `DtVpnStateListener`: + +| Estado | Significado | +|---|---| +| `DISCONNECTED` | VPN parada/inativa. | +| `CONNECTING` | App iniciando, conectando, resolvendo, atribuindo IP, adicionando rotas ou reconectando. | +| `AUTH` | Fase de autenticação SSH/VPN. | +| `CONNECTED` | Túnel conectado. | +| `DISCONNECTING` | Túnel parando. | +| `NO_NETWORK` | Aguardando rede. | +| `AUTH_FAILED` | Falha de autenticação. | + +### 5.4 APIs de consulta de usuário/conta + +| API | Assinatura | Retorno | Finalidade | +|---|---:|---|---| +| `DtStartCheckUser.execute()` | sem argumentos | void | Chama o `urlCheckUser` do perfil selecionado e envia o resultado para `dtCheckUserModelListener`. | + +A URL de check-user suporta estes placeholders: + +```txt +{username} +{user} +{password} +{uuid} +{hwid} +``` + +Exemplo de campo no perfil: + +```json +{ + "urlCheckUser": "https://example.com/checkuser.php?user={username}&uuid={uuid}&hwid={hwid}" +} +``` + +O payload esperado no callback é o que o seu servidor retornar. O app não força um schema fixo. Se a requisição falhar ou se não existir URL, o app envia um JSON fallback parecido com: + +```json +{ + "username": "current_user", + "expiration_date": "-", + "expiration_days": 0, + "count_connections": 0, + "limit_connections": 0, + "message": "" +} +``` + +### 5.5 APIs de dispositivo/sistema + +| API | Assinatura | Retorno | Finalidade | +|---|---:|---|---| +| `DtGetLocalIP.execute()` | sem argumentos | string | Retorna o IPv4 local, ou `0.0.0.0`/vazio quando indisponível. | +| `DtGetNetworkName.execute()` | sem argumentos | string | Retorna o nome exibido da rede/operadora ativa. | +| `DtGetPingResult.execute()` | sem argumentos | string | A versão atual retorna `N/A`. Temas não devem depender disso para ping real. | +| `DtGetStatusBarHeight.execute()` | sem argumentos | string numérica | Altura da status bar nativa do Android em pixels. | +| `DtGetNavigationBarHeight.execute()` | sem argumentos | string numérica | Altura da navigation bar nativa do Android em pixels. | +| `DtStartWebViewActivity.execute(url)` | string URL | void | Abre uma URL externamente/atividade WebView nativa. | +| `DtIgnoreBatteryOptimizations.execute()` | sem argumentos | void | Abre as configurações de otimização de bateria. | +| `DtStartApnActivity.execute()` | sem argumentos | void | Abre as configurações de APN. | +| `DtCleanApp.execute()` | sem argumentos | void | Abre o fluxo nativo de limpar dados/configurações. | +| `DtAirplaneState.execute()` | sem argumentos | string | Retorna o estado do modo avião. | +| `DtAirplaneActivate.execute()` | sem argumentos | void | Abre configurações do modo avião. Não ativa silenciosamente. | +| `DtAirplaneDeactivate.execute()` | sem argumentos | void | Abre configurações do modo avião. Não desativa silenciosamente. | + +### 5.6 APIs de proxy local / hotspot + +O bridge OnlineConfig expõe o serviço de proxy local usando nomes antigos de hotspot para compatibilidade com temas. + +| API | Assinatura | Retorno | Finalidade | +|---|---:|---|---| +| `DtGetStatusHotSpotService.execute()` | sem argumentos | `RUNNING` ou `STOPPED` | Lê o estado do serviço de proxy local. | +| `DtStartHotSpotService.execute()` | sem argumentos | void | Inicia o serviço de proxy local se estiver parado. | +| `DtStopHotSpotService.execute()` | sem argumentos | void | Para o serviço de proxy local se estiver rodando. | + +A porta padrão de proxy usada pelo tema existente é `6821`, mas o bridge em si só expõe iniciar/parar/estado e IP local. Se o tema mostrar um endereço de proxy, monte assim: + +```js +const proxyAddress = `${DtGetLocalIP.execute()}:6821`; +``` + +### 5.7 APIs de config visual/app + +| API | Assinatura | Retorno | Finalidade | +|---|---:|---|---| +| `DtGetAppConfig.execute(label)` | string label | string JSON ou null | Lê labels simples de configuração visual do app. | + +Labels atuais nesta versão do app: + +| Label | Retorno atual | +|---|---| +| `APP_LOGO` | `null` nesta versão do source. | +| `APP_BACKGROUND_TYPE` | `{"value":"COLOR"}` | +| `APP_BACKGROUND_COLOR` | `{"value":"#121212"}` | +| `APP_BACKGROUND_IMAGE` | `{"value":""}` | + +Uso seguro no tema: + +```js +function getAppConfigValue(label, fallback = '') { + try { + const raw = DtGetAppConfig.execute(label); + if (!raw) return fallback; + const data = JSON.parse(raw); + return data && data.value ? data.value : fallback; + } catch (e) { + return fallback; + } +} +``` + +--- + +## 6. Callbacks nativos que o tema pode definir + +Defina estes callbacks em `window` antes ou durante o script principal. O app chama eles com `evaluateJavascript`. + +### `window.DtVpnStateListener = function(state) {}` + +Chamado quando o status nativo da VPN muda e depois que a página termina de carregar. + +```js +window.DtVpnStateListener = function(state) { + updateStatusBadge(state); +}; +``` + +### `window.dtVpnStoppedSuccessListener = function() {}` + +Chamado depois que uma parada foi solicitada e o estado mapeado vira `DISCONNECTED`. + +```js +window.dtVpnStoppedSuccessListener = function() { + showToast('Desconectado'); +}; +``` + +### `window.DtUpdatePayloadSuccessListener = function(message) {}` + +Chamado depois que o app atualiza o catálogo/layout/cache de configs com sucesso. Nesta versão, a mensagem de sucesso normalmente é `OK`. + +```js +window.DtUpdatePayloadSuccessListener = function(message) { + reloadServerList(); +}; +``` + +### `window.DtUpdatePayloadErrorListener = function(message) {}` + +Chamado quando a atualização do catálogo falha. + +```js +window.DtUpdatePayloadErrorListener = function(message) { + alert(message || 'Falha ao atualizar'); +}; +``` + +### `window.dtCheckUserStartedListener = function() {}` + +Chamado antes da requisição nativa de check-user começar. + +```js +window.dtCheckUserStartedListener = function() { + showProfileLoading(); +}; +``` + +### `window.dtCheckUserModelListener = function(modelJson) {}` + +Chamado quando os dados do check-user estão prontos. `modelJson` é uma string. Faça parse se o endpoint retornar JSON. + +```js +window.dtCheckUserModelListener = function(modelJson) { + let data = {}; + try { data = JSON.parse(modelJson); } catch (e) { data = { message: modelJson }; } + renderProfile(data); +}; +``` + +--- + +## 7. JSON de config retornado para o HTML + +### 7.1 Estrutura de `DtGetConfigs.execute()` + +Retorna um array JSON de grupos. Cada grupo tem `items`. + +```json +[ + { + "id": "group_1", + "name": "TIM", + "color": "", + "sorter": 1, + "items": [ + { + "id": "profile_1", + "name": "TIM Principal", + "description": "WebSocket SSL", + "icon": "https://example.com/tim.png", + "mode": "SSH_PROXY", + "methodName": "", + "sorter": 1, + "status": "ACTIVE", + "auth": { + "username": true, + "password": true, + "v2ray_uuid": false, + "manual_username": true, + "manual_password": true, + "manual_v2ray_uuid": false + } + } + ] + } +] +``` + +### 7.2 Estrutura de `DtGetDefaultConfig.execute()` + +Retorna o objeto do perfil selecionado, ou `{}` se nada estiver selecionado. + +```json +{ + "id": "profile_1", + "name": "TIM Principal", + "description": "WebSocket SSL", + "icon": "https://example.com/tim.png", + "mode": "SSH_PROXY", + "methodName": "", + "auth": { + "username": true, + "password": true, + "v2ray_uuid": false, + "manual_username": true, + "manual_password": true, + "manual_v2ray_uuid": false + } +} +``` + +### 7.3 Significado do `auth` + +O objeto `auth` informa ao HTML quais inputs manuais o perfil selecionado espera. + +| Campo auth | Significado | +|---|---| +| `auth.username` | Mostrar/salvar input de usuário para perfis tipo SSH. | +| `auth.password` | Mostrar/salvar input de senha para perfis tipo SSH. | +| `auth.v2ray_uuid` | Mostrar/salvar input de UUID para perfis V2Ray/Xray. | +| `auth.manual_username` | Duplicado de compatibilidade de `auth.username`. | +| `auth.manual_password` | Duplicado de compatibilidade de `auth.password`. | +| `auth.manual_v2ray_uuid` | Duplicado de compatibilidade de `auth.v2ray_uuid`. | + +--- + +## 8. Selecionando servidores + +Monte sua lista de servidores a partir de `DtGetConfigs.execute()`, depois chame `DtSetConfig.execute(id)` quando o usuário selecionar um. + +```js +function loadConfigs() { + try { + return JSON.parse(DtGetConfigs.execute() || '[]'); + } catch (e) { + return []; + } +} + +function selectServer(profile) { + DtSetConfig.execute(String(profile.id)); + refreshCurrentServer(); +} + +function refreshCurrentServer() { + const current = JSON.parse(DtGetDefaultConfig.execute() || '{}'); + document.querySelector('#serverName').textContent = current.name || 'Selecionar servidor'; + updateAuthVisibility(current); +} +``` + +Depois da seleção, o app nativo também chama `CakeApp.applyAuthVisibility()` para esconder/mostrar automaticamente os campos de autenticação. + +--- + +## 9. Visibilidade dos campos de autenticação + +O app injeta `CakeApp.applyAuthVisibility()` e executa automaticamente no carregamento e depois da seleção de config. + +Ele varre todos os elementos `input`, `textarea` e `select`, e classifica eles por `id`, `name`, `class`, `placeholder` e `type`. + +Ele detecta: + +- Campos de UUID quando os metadados contêm `v2ray`, `uuid` ou `xray`. +- Campos de senha quando os metadados contêm `password`, `senha` ou `pass`. +- Campos de usuário quando os metadados contêm `username`, `usuario`, `login` ou tokens parecidos com usuário. + +Ele esconde o container pai mais próximo que bata com: + +```css +[data-auth-field], .form-group, .input-group, .field, .item, .row, .col, +[class*=field], [class*=input], [class*=form] +``` + +HTML recomendado: + +```html +
+ +
+ +
+ +
+ +
+ +
+``` + +### Limitação importante desta versão + +Nesta versão do `VOIDPRO.zip`, a função injetada de visibilidade considera V2Ray/Xray somente quando `mode` começa com `v2ray` ou `xray`. + +Isto funciona: + +```json +{ "mode": "V2RAY" } +``` + +Isto funciona: + +```json +{ "mode": "XRAY" } +``` + +Estes podem não ser detectados pela função injetada, a não ser que o tema tenha sua própria lógica de visibilidade ou você aplique patch no Java: + +```json +{ "mode": "VMESS" } +{ "mode": "VLESS" } +{ "mode": "V2RAY - VMESS" } +``` + +Para builds atuais, prefira este formato de perfil no catálogo para configs baseadas em UUID: + +```json +{ + "id": "v2ray_1", + "name": "Servidor V2Ray", + "mode": "V2RAY", + "auth": { + "username": false, + "password": false, + "v2ray_uuid": true + } +} +``` + +### Helper forte de visibilidade pelo lado do tema + +Use isto no tema se quiser exibição confiável do UUID para nomes `vmess`, `vless`, `xray` e `v2ray`. + +```js +function isLocked(value) { + return String(value ?? '').trim().toLowerCase() === 'locked'; +} + +function isUuidProfile(config) { + const mode = String(config?.mode || '').toLowerCase(); + const auth = config?.auth || {}; + return auth.v2ray_uuid === true || + auth.manual_v2ray_uuid === true || + mode.includes('v2ray') || + mode.includes('xray') || + mode.includes('vmess') || + mode.includes('vless'); +} + +function updateAuthVisibility(config = null) { + if (!config) { + try { config = JSON.parse(DtGetDefaultConfig.execute() || '{}'); } + catch (e) { config = {}; } + } + + const uuidRequired = isUuidProfile(config) && !isLocked(DtUuid.get?.()); + const sshRequired = !uuidRequired; + + document.querySelector('#uuid-group').style.display = uuidRequired ? '' : 'none'; + document.querySelector('#username-group').style.display = sshRequired ? '' : 'none'; + document.querySelector('#password-group').style.display = sshRequired ? '' : 'none'; +} +``` + +--- + +## 10. Iniciando e parando a conexão + +Padrão básico: + +```js +function saveVisibleInputs() { + const username = document.querySelector('#username')?.value || ''; + const password = document.querySelector('#password')?.value || ''; + const uuid = document.querySelector('#v2ray_uuid')?.value || ''; + + if (username) DtUsername.set(username); + if (password) DtPassword.set(password); + if (uuid) DtUuid.set(uuid); +} + +function connectOrDisconnect() { + const state = DtGetVpnState.execute(); + + if (state === 'CONNECTED' || state === 'CONNECTING' || state === 'AUTH') { + DtExecuteVpnStop.execute(); + return; + } + + const current = JSON.parse(DtGetDefaultConfig.execute() || '{}'); + if (!current.id) { + alert('Selecione um servidor primeiro.'); + return; + } + + saveVisibleInputs(); + DtExecuteVpnStart.execute(); +} +``` + +Desenvolvedores de temas devem controlar o estado da UI com `DtVpnStateListener`, não apenas com eventos de clique. Mudanças nativas de estado podem acontecer fora do clique do botão. + +--- + +## 11. Logs + +`DtGetLogs.execute()` retorna um array JSON. Cada item é um objeto com uma chave de timestamp e um valor de mensagem. + +Exemplo: + +```json +[ + { "14:03:55": "Connecting" }, + { "14:03:56": "Authenticating" }, + { "14:03:57": "Server Message: welcome" } +] +``` + +Helper de renderização: + +```js +function readLogs() { + let rows = []; + try { rows = JSON.parse(DtGetLogs.execute() || '[]'); } catch (e) {} + return rows.map(row => { + const time = Object.keys(row)[0] || ''; + const message = row[time] || ''; + return `${time} ${message}`; + }).join('\n'); +} +``` + +O buffer de logs nativo exposto ao HTML é limitado a entradas recentes de status e mensagens de banner do servidor. Ele não é o logcat/debug completo. + +--- + +## 12. Modal de perfil / check-user + +Fluxo recomendado de perfil: + +```js +function openProfile() { + showProfileLoading(); + DtStartCheckUser.execute(); +} + +window.dtCheckUserStartedListener = function() { + showProfileLoading(); +}; + +window.dtCheckUserModelListener = function(modelJson) { + let data; + try { data = JSON.parse(modelJson); } + catch (e) { data = { message: modelJson }; } + + renderProfile({ + username: data.username || '-', + expirationDate: data.expiration_date || '-', + expirationDays: data.expiration_days ?? '-', + connections: `${data.count_connections ?? 0}/${data.limit_connections ?? '-'}`, + message: data.message || '' + }); +}; +``` + +Formato comum de resposta do seu painel/API check: + +```json +{ + "username": "utest678", + "count_connections": 1, + "expiration_date": "25/04/2026", + "expiration_days": 0, + "limit_connections": 1 +} +``` + +--- + +## 13. Botão de atualizar configs + +Use: + +```js +function updateConfigs() { + DtStartAppUpdate.execute(); +} + +window.DtUpdatePayloadSuccessListener = function(message) { + reloadServerList(); + alert('Configs atualizadas'); +}; + +window.DtUpdatePayloadErrorListener = function(message) { + alert(message || 'Falha ao atualizar configs'); +}; +``` + +Depois de uma atualização bem-sucedida, o app recarrega o layout atual. Não guarde estado crítico ainda não salvo apenas em memória. + +--- + +## 14. Dimensões de layout e áreas seguras + +Use as alturas nativas das barras junto com variáveis CSS de safe area para melhor compatibilidade entre dispositivos. + +```js +function updateInsets() { + const status = Number(DtGetStatusBarHeight?.execute?.() || 0); + const nav = Number(DtGetNavigationBarHeight?.execute?.() || 0); + + document.documentElement.style.setProperty('--status-bar-height', `${status}px`); + document.documentElement.style.setProperty('--nav-bar-height', `${Math.min(nav, 48)}px`); +} + +window.addEventListener('resize', updateInsets); +document.addEventListener('DOMContentLoaded', updateInsets); +``` + +Exemplo CSS: + +```css +.header { + padding-top: max(var(--status-bar-height, 0px), env(safe-area-inset-top)); +} + +.footer { + padding-bottom: max(12px, env(safe-area-inset-bottom)); +} +``` + +--- + +## 15. Campos do catálogo OnlineConfig relevantes para temas + +O endpoint de catálogo do painel é consumido pelo app e depois normalizado para o HTML. Autores de temas normalmente não chamam esse endpoint diretamente, mas conhecer a estrutura de origem ajuda a evitar UI quebrada. + +O catálogo de nível superior pode conter: + +```json +{ + "title": "My VPN", + "selectedGroupId": "tim", + "selectedProfileId": "tim_ssl", + "appLogoUrl": "https://example.com/logo.png", + "globalCustomLayoutHtml": "...", + "groups": [] +} +``` + +Grupos contêm perfis: + +```json +{ + "id": "tim", + "name": "TIM", + "color": "#0055ff", + "sorter": 1, + "profiles": [] +} +``` + +Perfis contêm: + +```json +{ + "id": "tim_ssl", + "name": "TIM SSL", + "description": "SSL + payload", + "methodName": "SSL", + "mode": "SSL_PROXY", + "icon": "https://example.com/tim.png", + "sorter": 1, + "auth": { + "username": true, + "password": true, + "v2ray_uuid": false + }, + "urlCheckUser": "https://example.com/check?user={username}", + "configUrl": "https://example.com/profile/tim_ssl", + "configBase64": "...", + "cachedSettingsJson": "..." +} +``` + +O HTML recebe os perfis como `items`, não como `profiles`, ao chamar `DtGetConfigs.execute()`. + +--- + +## 16. Campos de payload/settings do perfil que afetam o bridge + +Quando um perfil é selecionado, o app aplica um arquivo/base64 de config ou um payload JSON de settings. + +Os campos de settings abaixo importam para o que o bridge HTML vai reportar depois: + +| Campo settings | Aliases | Usado para | +|---|---|---| +| `mode` | `tunnelType` | Detecção nativa do tipo de túnel. | +| `serverHost` | `sshServer`, `server_host` | Host SSH/servidor. | +| `serverPort` | `sshPort`, `server_port` | Porta SSH/servidor. | +| `username` | `sshUser`, `user` | Usuário SSH. Vazio significa que credencial manual pode ser necessária. | +| `password` | `sshPass`, `pass` | Senha SSH. Vazio significa que credencial manual pode ser necessária. | +| `xrayUuid` | `v2ray_uuid`, `uuid` | UUID V2Ray/Xray. Vazio significa que UUID manual pode ser necessário. | +| `xrayConfig` | `config_v2ray`, `v2rayConfig` | Config JSON V2Ray/Xray. | +| `payload` | `customPayload`, `proxyPayload` | Payload SSH customizado. | +| `proxyHost` | `proxy_host` | Host do proxy. | +| `proxyPort` | `proxy_port` | Porta do proxy. | +| `sni` | `sslSni`, `customSni` | TLS/SNI. | +| `slowNameServer` | `nameServer`, `ns`, `nameserver` | Nameserver SlowDNS. | +| `slowDnsServer` | `dns`, `dnsServer`, `slowDns` | Servidor DNS SlowDNS. | +| `slowDnsKey` | `key`, `slowKey` | Chave SlowDNS. | + +Valores atuais de tipo de túnel reconhecidos pelo parser de settings do app: + +| Valor | Significado | +|---|---| +| `1`, `SSH_DIRECT` | SSH direto. | +| `2`, `SSH_PROXY` | SSH proxy. | +| `3`, `SSL_DIRECT`, `SSH_SSL` | SSH SSL. | +| `4`, `SLOW_DNS`, `DNSTT` | SlowDNS/DNSTT. | +| `5`, `SSL_PROXY`, `SSH_SSL_PROXY` | SSH SSL proxy. | +| `6`, `V2RAY`, `XRAY` | Xray/V2Ray. | + +Para perfis V2Ray com UUID, use `mode: "V2RAY"` ou `tunnelType: 6` no payload de settings. + +Evite enviar boolean `true` como `auth.v2ray_uuid` dentro do payload real de settings. Em settings, `v2ray_uuid` é tratado como alias de valor do UUID. No auth do catálogo, `auth.v2ray_uuid` é boolean. Mantenha os dois conceitos separados: + +Auth do perfil no catálogo: + +```json +"auth": { "v2ray_uuid": true } +``` + +Valor UUID no payload de settings: + +```json +"xrayUuid": "" +``` + +--- + +## 17. Teste no navegador sem bridge Android + +Use mocks somente enquanto desenvolve no navegador. Como o app remove a seção mock depois do marcador `// --- AMBIENTE MOCK PARA TESTES ---`, coloque mocks no final do HTML abaixo desse marcador exato, ou use um arquivo local separado somente para teste. + +Mock mínimo para navegador: + +```js +if (!window.DtGetVpnState) { + window.DtGetVpnState = { execute: () => 'DISCONNECTED' }; + window.DtGetConfigs = { execute: () => JSON.stringify([]) }; + window.DtGetDefaultConfig = { execute: () => '{}' }; + window.DtSetConfig = { execute: (id) => console.log('select', id) }; + window.DtUsername = { get: () => '', set: (v) => console.log('username', v) }; + window.DtPassword = { get: () => '', set: (v) => console.log('password', v) }; + window.DtUuid = { get: () => '', set: (v) => console.log('uuid', v) }; + window.DtExecuteVpnStart = { execute: () => console.log('start') }; + window.DtExecuteVpnStop = { execute: () => console.log('stop') }; +} +``` + +--- + +## 18. Erros comuns em temas + +### Erro: ler JSON sem `try/catch` + +Ruim: + +```js +const configs = JSON.parse(DtGetConfigs.execute()); +``` + +Bom: + +```js +let configs = []; +try { configs = JSON.parse(DtGetConfigs.execute() || '[]'); } catch (e) {} +``` + +### Erro: mostrar `Locked` nos inputs + +Ruim: + +```js +usernameInput.value = DtUsername.get(); +``` + +Bom: + +```js +usernameInput.value = cleanBridgeValue(DtUsername.get()); +``` + +### Erro: assumir que todos os modos V2Ray são detectados automaticamente pelo código de visibilidade injetado + +Use `mode: "V2RAY"` no catálogo/settings para este build, ou adicione um helper de visibilidade no tema que também verifique `vmess` e `vless`. + +### Erro: salvar autenticação somente no conectar + +O usuário pode trocar de tela ou usar conexão automática. Salve também quando o input mudar: + +```js +usernameInput.addEventListener('input', () => DtUsername.set(usernameInput.value)); +passwordInput.addEventListener('input', () => DtPassword.set(passwordInput.value)); +uuidInput.addEventListener('input', () => DtUuid.set(uuidInput.value)); +``` + +### Erro: depender do estado do clique em vez do estado nativo + +Sempre escute: + +```js +window.DtVpnStateListener = function(state) { + renderVpnState(state); +}; +``` + +### Erro: usar objetos nativos sem verificar se existem + +Temas podem ser abertos fora do Android para preview. Sempre proteja chamadas do bridge. + +--- + +## 19. Wrapper recomendado para APIs do tema + +Coloque isto perto do começo de todo layout customizado. + +```js +const OnlineBridge = { + exec(name, fallback = '') { + try { + const obj = window[name]; + if (obj && typeof obj.execute === 'function') return obj.execute(); + } catch (e) {} + return fallback; + }, + + command(name, value) { + try { + const obj = window[name]; + if (obj && typeof obj.execute === 'function') { + if (arguments.length >= 2) obj.execute(String(value)); + else obj.execute(); + } + } catch (e) {} + }, + + get(name, fallback = '') { + try { + const obj = window[name]; + if (obj && typeof obj.get === 'function') return obj.get(); + } catch (e) {} + return fallback; + }, + + set(name, value) { + try { + const obj = window[name]; + if (obj && typeof obj.set === 'function') obj.set(String(value ?? '')); + } catch (e) {} + }, + + jsonFromExec(name, fallback) { + try { + return JSON.parse(this.exec(name, '') || ''); + } catch (e) { + return fallback; + } + }, + + clean(value) { + const text = String(value ?? '').trim(); + return text.toLowerCase() === 'locked' ? '' : text; + }, + + getConfigs() { + return this.jsonFromExec('DtGetConfigs', []); + }, + + getCurrentConfig() { + return this.jsonFromExec('DtGetDefaultConfig', {}); + }, + + selectConfig(id) { + this.command('DtSetConfig', id); + }, + + getState() { + return this.exec('DtGetVpnState', 'DISCONNECTED'); + }, + + start() { + this.command('DtExecuteVpnStart'); + }, + + stop() { + this.command('DtExecuteVpnStop'); + }, + + getAuth() { + return { + username: this.clean(this.get('DtUsername')), + password: this.clean(this.get('DtPassword')), + uuid: this.clean(this.get('DtUuid')) + }; + }, + + saveAuth({ username, password, uuid }) { + this.set('DtUsername', username || ''); + this.set('DtPassword', password || ''); + this.set('DtUuid', uuid || ''); + } +}; +``` + +--- + +## 20. Checklist mínimo para um novo tema HTML OnlineConfig + +Antes de publicar um tema, verifique: + +- Funciona quando `DtGetDefaultConfig.execute()` retorna `{}`. +- Não mostra `Locked` para o usuário. +- Consegue fazer parse seguro de JSON vazio ou inválido. +- Escuta `DtVpnStateListener`. +- Chama `DtSetConfig.execute(id)` antes de iniciar um perfil selecionado. +- Salva usuário/senha/UUID antes de conectar. +- Trata `auth.username`, `auth.password` e `auth.v2ray_uuid`. +- O campo UUID aparece para configs `mode: "V2RAY"` / `mode: "XRAY"`. +- Não depende de `DtGetPingResult` retornar ping real. +- Não coloca código de produção abaixo do marcador de mock. +- Usa APIs nativas de altura das barras ou CSS safe areas para a navegação inferior não ficar cortada.