feat(mobile): scaffold Capacitor pra Android Play Store + adapter nativo
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) <noreply@anthropic.com>
This commit is contained in:
parent
ca9de52ae1
commit
7a523b8873
7 changed files with 412 additions and 2 deletions
|
|
@ -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(){
|
function initServiceWorker(){
|
||||||
|
if(isNative())return; // Capacitor não usa SW (tem cache próprio + APIs nativas)
|
||||||
if(!('serviceWorker' in navigator))return;
|
if(!('serviceWorker' in navigator))return;
|
||||||
navigator.serviceWorker.register('/sw.js').then(reg=>{
|
navigator.serviceWorker.register('/sw.js').then(reg=>{
|
||||||
console.log('[SW] registered:',reg.scope);
|
console.log('[SW] registered:',reg.scope);
|
||||||
|
|
|
||||||
19
mobile/.gitignore
vendored
Normal file
19
mobile/.gitignore
vendored
Normal file
|
|
@ -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
|
||||||
213
mobile/README.md
Normal file
213
mobile/README.md
Normal file
|
|
@ -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 `<manifest>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.sensor.barometer" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.sensor.compass" android:required="false" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 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
|
||||||
|
```
|
||||||
44
mobile/capacitor.config.json
Normal file
44
mobile/capacitor.config.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
mobile/package.json
Normal file
27
mobile/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
scripts/sync-html.mjs
Normal file
25
scripts/sync-html.mjs
Normal file
|
|
@ -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); });
|
||||||
|
|
@ -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(){
|
function initServiceWorker(){
|
||||||
|
if(isNative())return;
|
||||||
if(!('serviceWorker' in navigator))return;
|
if(!('serviceWorker' in navigator))return;
|
||||||
navigator.serviceWorker.register('/sw.js').then(reg=>{
|
navigator.serviceWorker.register('/sw.js').then(reg=>{
|
||||||
console.log('[SW] registered:',reg.scope);
|
console.log('[SW] registered:',reg.scope);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue