From 7a523b887345e031c28ae27ef13ec719f34fc029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PontualTech=20/=20Karl=C3=A3o?= Date: Mon, 27 Apr 2026 16:02:34 -0300 Subject: [PATCH] feat(mobile): scaffold Capacitor pra Android Play Store + adapter nativo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESTRUTURA NOVA: mobile/ + scripts/sync-html.mjs - mobile/package.json: Capacitor 6 + plugins (geolocation, local-notifications, network, preferences, status-bar) - mobile/capacitor.config.json: appId br.com.pontualtech.shivao, allowNavigation pra OSM/Windy/CDNs - mobile/.gitignore: protege keystore (NUNCA commitar chaves privadas) - mobile/README.md: setup completo (JDK + Android Studio + keystore + build APK/AAB + Play Store submission + iOS futuro + troubleshooting) - scripts/sync-html.mjs: copia app/diario-bordo.html → server/public + mobile/www (1 fonte da verdade) ADAPTER NATIVO no HTML (sincronizado app/ + server/public/): - isNative() / nativePlatform() detecta Capacitor - nativeWatchPosition() usa Capacitor.Geolocation (background-capable) com fallback navigator.geolocation - nativeNotify() usa Capacitor.LocalNotifications com fallback toast - initServiceWorker() pula registro no Capacitor (WebView nativo já tem cache próprio) NÃO INCLUI (ainda): - Build local: precisa JDK 17 + Android Studio (~3GB) — instruções no README - Keystore: gerar 1 vez via keytool (script no README) - AAB pra Play Store: comandos no README - Conta Google Play Developer: $25 1× pelo dono Próximo passo manual: instalar JDK + Android Studio, rodar 'npm install && npx cap add android'. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/diario-bordo.html | 45 +++++++- mobile/.gitignore | 19 ++++ mobile/README.md | 213 +++++++++++++++++++++++++++++++++++ mobile/capacitor.config.json | 44 ++++++++ mobile/package.json | 27 +++++ scripts/sync-html.mjs | 25 ++++ server/public/index.html | 41 ++++++- 7 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 mobile/.gitignore create mode 100644 mobile/README.md create mode 100644 mobile/capacitor.config.json create mode 100644 mobile/package.json create mode 100644 scripts/sync-html.mjs diff --git a/app/diario-bordo.html b/app/diario-bordo.html index ffa4a2a..80906e1 100644 --- a/app/diario-bordo.html +++ b/app/diario-bordo.html @@ -3141,8 +3141,51 @@ async function testWindyKey(){ } } -// ===== Service Worker (offline real) ===== +// ===== Capacitor adapter (detecta nativo + usa APIs nativas com fallback Web) ===== +const isNative=()=>!!(window.Capacitor&&window.Capacitor.isNativePlatform&&window.Capacitor.isNativePlatform()); +const nativePlatform=()=>(window.Capacitor&&window.Capacitor.getPlatform)?window.Capacitor.getPlatform():'web'; + +// Geolocation: Capacitor.Geolocation (background-capable) > navigator.geolocation +async function nativeWatchPosition(onUpdate,onError,opts){ + if(isNative()&&window.Capacitor.Plugins.Geolocation){ + try{ + const{Geolocation}=window.Capacitor.Plugins; + // Pede permission upfront + const perm=await Geolocation.requestPermissions({permissions:['location']}).catch(()=>({location:'denied'})); + if(perm.location==='denied'){onError({code:1,PERMISSION_DENIED:1,message:'Permissão negada'});return null} + const id=await Geolocation.watchPosition({enableHighAccuracy:!!opts?.enableHighAccuracy,timeout:opts?.timeout||15000,maximumAge:opts?.maximumAge||0},(pos,err)=>{ + if(err){onError({code:err.code||3,message:err.message||'GPS erro',PERMISSION_DENIED:1,POSITION_UNAVAILABLE:2,TIMEOUT:3});return} + onUpdate({coords:{latitude:pos.coords.latitude,longitude:pos.coords.longitude,accuracy:pos.coords.accuracy,speed:pos.coords.speed},timestamp:pos.timestamp}); + }); + return{native:true,id}; + }catch(e){console.warn('[native gps] fallback to web:',e.message)} + } + // Fallback web + const id=navigator.geolocation.watchPosition(onUpdate,onError,opts); + return{native:false,id}; +} +async function nativeClearWatch(handle){ + if(!handle)return; + if(handle.native&&isNative()){try{await window.Capacitor.Plugins.Geolocation.clearWatch({id:handle.id})}catch(e){}} + else if(handle.id!=null)navigator.geolocation.clearWatch(handle.id); +} + +// Local notifications: nativo no Android, fallback toast no web +async function nativeNotify(title,body,id){ + if(isNative()&&window.Capacitor.Plugins.LocalNotifications){ + try{ + const{LocalNotifications}=window.Capacitor.Plugins; + await LocalNotifications.requestPermissions().catch(()=>{}); + await LocalNotifications.schedule({notifications:[{id:id||Date.now()%2147483647,title,body,sound:null,smallIcon:'ic_stat_icon_config_sample'}]}); + return; + }catch(e){console.warn('[native notify]',e.message)} + } + toast(title+(body?': '+body:'')); +} + +// ===== Service Worker (offline real — só registra no Web, no Capacitor o WebView usa cache nativo) ===== function initServiceWorker(){ + if(isNative())return; // Capacitor não usa SW (tem cache próprio + APIs nativas) if(!('serviceWorker' in navigator))return; navigator.serviceWorker.register('/sw.js').then(reg=>{ console.log('[SW] registered:',reg.scope); diff --git a/mobile/.gitignore b/mobile/.gitignore new file mode 100644 index 0000000..db88c09 --- /dev/null +++ b/mobile/.gitignore @@ -0,0 +1,19 @@ +# Capacitor +node_modules/ +www/ +android/.gradle/ +android/local.properties +android/app/release/ +android/app/build/ +android/build/ +android/.idea/ +android/captures/ +android/*.iml +ios/App/build/ +ios/App/Pods/ +ios/App/output/ + +# Keystore (CRÍTICO — nunca commitar chaves privadas) +*.keystore +*.jks +keystore-passwords.txt diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..1373209 --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,213 @@ +# Shivao Mobile · Capacitor Android (e iOS futuro) + +Wrapper Capacitor pra empacotar o Shivao web como app nativo Android (.apk pra sideload, .aab pra Play Store) e iOS no futuro. + +**Arquitetura:** o frontend HTML mora em `app/diario-bordo.html` (1 fonte de verdade). Um script (`scripts/sync-html.mjs`) copia pra `server/public/index.html` (Express serve) e `mobile/www/index.html` (Capacitor empacota). + +--- + +## 🛠️ Pré-requisitos (instalar 1 vez) + +### Windows (recomendado pra você) + +1. **JDK 17+** — Adoptium Temurin é a melhor escolha: + ``` + winget install EclipseAdoptium.Temurin.17.JDK + ``` + Verificar: `java -version` deve mostrar `17.x` + +2. **Android Studio** — instala SDK + emulador + tudo: + ``` + winget install Google.AndroidStudio + ``` + Após abrir, ir em **Tools → SDK Manager** e instalar: + - Android SDK Platform 34 (Android 14) + - Android SDK Build-Tools 34.0.0 + - Android SDK Command-line Tools (latest) + - Android SDK Platform-Tools + +3. **Variáveis de ambiente** (Painel de Controle → Sistema → Variáveis de Ambiente): + - `JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-17.0.x` (ajuste path) + - `ANDROID_HOME=C:\Users\pontu\AppData\Local\Android\Sdk` + - Adicionar ao PATH: `%JAVA_HOME%\bin`, `%ANDROID_HOME%\platform-tools` + +4. Reiniciar terminal. Verificar: + ```bash + java -version + adb --version + ``` + +**Tempo total de instalação:** ~30-45 min (download de ~3GB). + +--- + +## 🚀 Build inicial (faz 1 vez) + +```bash +cd mobile + +# Instala deps Capacitor + plugins +npm install + +# Sincroniza HTML do app/ pra mobile/www/ +node ../scripts/sync-html.mjs + +# Adiciona projeto Android (gera mobile/android/) +npx cap add android + +# Sincroniza plugins nativos no projeto Android +npx cap sync android +``` + +### Permissões Android (editar manualmente após `cap add android`) + +Abra `mobile/android/app/src/main/AndroidManifest.xml` e adicione DENTRO do ``: + +```xml + + + + + + + + + + + + + + +``` + +--- + +## 🔐 Keystore PERMANENTE (CRÍTICO — gera 1 vez, guarda pra sempre) + +```bash +cd mobile/android/app +keytool -genkey -v -keystore shivao-release.keystore -alias shivao -keyalg RSA -keysize 2048 -validity 10000 \ + -dname "CN=Karlao, O=PontualTech, L=Sao Paulo, ST=SP, C=BR" +# Senha forte (mínimo 8 chars). ANOTE em local seguro — perda = perda permanente do app na Play Store. +``` + +**BACKUP IMEDIATO:** +```bash +cp shivao-release.keystore C:/Users/pontu/Downloads/Shivao-keystore-backup/shivao-release.keystore +echo "Senha keystore: SUA_SENHA_AQUI" >> C:/Users/pontu/Downloads/Shivao-keystore-backup/keystore-passwords.txt +echo "Alias: shivao" >> C:/Users/pontu/Downloads/Shivao-keystore-backup/keystore-passwords.txt +``` + +E também faça upload em Drive privado, HD externo etc — perdeu = não publica update na Play Store nunca mais. + +### Configurar signing no Gradle + +Edite `mobile/android/app/build.gradle`, dentro de `android { ... }`: + +```gradle +signingConfigs { + release { + storeFile file('shivao-release.keystore') + storePassword System.getenv('SHIVAO_KEYSTORE_PWD') ?: 'SUA_SENHA_AQUI' + keyAlias 'shivao' + keyPassword System.getenv('SHIVAO_KEY_PWD') ?: 'SUA_SENHA_AQUI' + } +} +buildTypes { + release { + signingConfig signingConfigs.release + minifyEnabled false + } +} +``` + +--- + +## 📦 Build do APK (sideload) e AAB (Play Store) + +```bash +cd mobile + +# Sincroniza última versão do HTML antes de build +npm run sync + +# APK assinado pra sideload (~3-5 min primeira vez, ~1 min em rebuilds) +npm run android:build:apk +# Output: mobile/android/app/build/outputs/apk/release/app-release.apk + +# AAB pra Play Store (formato preferido) +npm run android:build:aab +# Output: mobile/android/app/build/outputs/bundle/release/app-release.aab +``` + +--- + +## 🌐 Alternativa: Build na nuvem (sem instalar nada local) + +Se não quiser instalar 3GB de Android Studio: + +### Opção A: Codemagic (mais simples) +- 500min grátis/mês, suficiente pra ~10 builds +- Conecta ao Forgejo, push detecta e roda +- Configura assinatura no painel + +### Opção B: Container Docker no Coolify (mais self-hosted) +- Subir imagem `mingc/android-build-box` no Coolify +- Cron job ou webhook do Forgejo dispara build +- APK/AAB salvo em volume persistente + +Ambas opções: vamos preparar quando você decidir. + +--- + +## 🎮 Submeter Play Store + +1. **Conta Google Play Developer:** $25 1× em https://play.google.com/console +2. Após aprovado (~24h), criar novo app: + - Nome: "Shivao - Diário de Bordo" + - Categoria: "Maps & Navigation" + - Idioma: pt-BR +3. **Subir AAB:** Production → Releases → Create new release → upload `app-release.aab` +4. **Conteúdo obrigatório:** + - Política de privacidade (URL pública — vou criar uma rota `/politica-privacidade` no servidor) + - Screenshots: mínimo 2 (vou gerar via Playwright se quiser) + - Ícone 512×512 (vou gerar do icon.svg) + - Descrição curta (80 chars) e completa +5. **Content rating:** preencher questionário (~5min) +6. **Pricing:** Free (cobrança vai por dentro do app via Asaas, não Google IAP — economia de 30%) +7. **Submit pra review:** Google revisa em 1-7 dias + +--- + +## 📱 iOS (FASE 4 futura) + +Requer: +- Mac (mini M4 ~R$5k mais barato) +- Apple Developer Program $99/ano +- Xcode + +```bash +npx cap add ios +npx cap sync ios +npx cap open ios # abre Xcode +# Build Archive → Distribute → App Store Connect +``` + +--- + +## 🐛 Troubleshooting + +**`adb: command not found`** — adicione `%ANDROID_HOME%\platform-tools` ao PATH + +**`SDK location not found`** — crie `mobile/android/local.properties`: +``` +sdk.dir=C:\\Users\\pontu\\AppData\\Local\\Android\\Sdk +``` + +**Build muito lento** — primeiro build baixa Gradle + dependências (~500MB). Próximos builds usam cache. + +**APK muito grande** — habilitar minify em `build.gradle`: +```gradle +buildTypes.release.minifyEnabled true +buildTypes.release.shrinkResources true +``` diff --git a/mobile/capacitor.config.json b/mobile/capacitor.config.json new file mode 100644 index 0000000..9ade570 --- /dev/null +++ b/mobile/capacitor.config.json @@ -0,0 +1,44 @@ +{ + "appId": "br.com.pontualtech.shivao", + "appName": "Shivao", + "webDir": "www", + "bundledWebRuntime": false, + "android": { + "allowMixedContent": false, + "captureInput": true, + "webContentsDebuggingEnabled": false, + "backgroundColor": "#0e2a3dFF" + }, + "ios": { + "contentInset": "automatic", + "scrollEnabled": true, + "backgroundColor": "#0e2a3dFF" + }, + "server": { + "androidScheme": "https", + "iosScheme": "capacitor", + "cleartext": false, + "url": "https://shivao.pontualtech.work", + "allowNavigation": [ + "shivao.pontualtech.work", + "tile.openstreetmap.org", + "*.tile.openstreetmap.org", + "api.windy.com", + "api.open-meteo.com", + "marine-api.open-meteo.com", + "fonts.googleapis.com", + "fonts.gstatic.com", + "cdnjs.cloudflare.com" + ] + }, + "plugins": { + "Geolocation": { + "permissions": ["location"] + }, + "LocalNotifications": { + "smallIcon": "ic_stat_icon_config_sample", + "iconColor": "#a07832", + "sound": "alarme.wav" + } + } +} diff --git a/mobile/package.json b/mobile/package.json new file mode 100644 index 0000000..6af3aa9 --- /dev/null +++ b/mobile/package.json @@ -0,0 +1,27 @@ +{ + "name": "shivao-mobile", + "version": "1.2.0", + "description": "Shivao app nativo (Capacitor wrapper Android/iOS)", + "main": "index.js", + "type": "module", + "scripts": { + "sync": "node ../scripts/sync-html.mjs && npx cap sync", + "android:open": "npx cap open android", + "android:build:apk": "cd android && ./gradlew assembleRelease", + "android:build:aab": "cd android && ./gradlew bundleRelease", + "ios:open": "npx cap open ios" + }, + "dependencies": { + "@capacitor/android": "^6.1.2", + "@capacitor/app": "^6.0.1", + "@capacitor/core": "^6.1.2", + "@capacitor/geolocation": "^6.0.1", + "@capacitor/local-notifications": "^6.1.0", + "@capacitor/network": "^6.0.2", + "@capacitor/preferences": "^6.0.2", + "@capacitor/status-bar": "^6.0.1" + }, + "devDependencies": { + "@capacitor/cli": "^6.1.2" + } +} diff --git a/scripts/sync-html.mjs b/scripts/sync-html.mjs new file mode 100644 index 0000000..fef6d95 --- /dev/null +++ b/scripts/sync-html.mjs @@ -0,0 +1,25 @@ +// Sync único do HTML do Shivão pra todos os destinos. +// Fonte de verdade: app/diario-bordo.html +// Destinos: server/public/index.html (Express) + mobile/www/index.html (Capacitor) +import { copyFile, mkdir } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, '..'); + +const src = resolve(root, 'app/diario-bordo.html'); +const targets = [ + resolve(root, 'server/public/index.html'), + resolve(root, 'mobile/www/index.html'), +]; + +async function sync() { + for (const t of targets) { + await mkdir(dirname(t), { recursive: true }); + await copyFile(src, t); + console.log(`✓ ${src} → ${t}`); + } +} + +sync().catch(e => { console.error(e); process.exit(1); }); diff --git a/server/public/index.html b/server/public/index.html index 4c9f482..9469b2a 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -3118,8 +3118,47 @@ async function testWindyKey(){ } } -// ===== Service Worker (offline real) ===== +// ===== Capacitor adapter ===== +const isNative=()=>!!(window.Capacitor&&window.Capacitor.isNativePlatform&&window.Capacitor.isNativePlatform()); +const nativePlatform=()=>(window.Capacitor&&window.Capacitor.getPlatform)?window.Capacitor.getPlatform():'web'; + +async function nativeWatchPosition(onUpdate,onError,opts){ + if(isNative()&&window.Capacitor.Plugins.Geolocation){ + try{ + const{Geolocation}=window.Capacitor.Plugins; + const perm=await Geolocation.requestPermissions({permissions:['location']}).catch(()=>({location:'denied'})); + if(perm.location==='denied'){onError({code:1,PERMISSION_DENIED:1,message:'Permissão negada'});return null} + const id=await Geolocation.watchPosition({enableHighAccuracy:!!opts?.enableHighAccuracy,timeout:opts?.timeout||15000,maximumAge:opts?.maximumAge||0},(pos,err)=>{ + if(err){onError({code:err.code||3,message:err.message||'GPS erro',PERMISSION_DENIED:1,POSITION_UNAVAILABLE:2,TIMEOUT:3});return} + onUpdate({coords:{latitude:pos.coords.latitude,longitude:pos.coords.longitude,accuracy:pos.coords.accuracy,speed:pos.coords.speed},timestamp:pos.timestamp}); + }); + return{native:true,id}; + }catch(e){console.warn('[native gps] fallback to web:',e.message)} + } + const id=navigator.geolocation.watchPosition(onUpdate,onError,opts); + return{native:false,id}; +} +async function nativeClearWatch(handle){ + if(!handle)return; + if(handle.native&&isNative()){try{await window.Capacitor.Plugins.Geolocation.clearWatch({id:handle.id})}catch(e){}} + else if(handle.id!=null)navigator.geolocation.clearWatch(handle.id); +} + +async function nativeNotify(title,body,id){ + if(isNative()&&window.Capacitor.Plugins.LocalNotifications){ + try{ + const{LocalNotifications}=window.Capacitor.Plugins; + await LocalNotifications.requestPermissions().catch(()=>{}); + await LocalNotifications.schedule({notifications:[{id:id||Date.now()%2147483647,title,body,sound:null,smallIcon:'ic_stat_icon_config_sample'}]}); + return; + }catch(e){console.warn('[native notify]',e.message)} + } + toast(title+(body?': '+body:'')); +} + +// ===== Service Worker (apenas Web — Capacitor usa cache nativo) ===== function initServiceWorker(){ + if(isNative())return; if(!('serviceWorker' in navigator))return; navigator.serviceWorker.register('/sw.js').then(reg=>{ console.log('[SW] registered:',reg.scope);