feat: v0.2.0 — sincronização do botão garantida, SSE em tempo real, WYSIWYG/Preview e lista rica

Sync (bug do celular real):
- mudanças só de metadados (título/formato/categorias) entram na fila
- auto-reconciliação: toda rodada enfileira notas sujas sem entrada na fila
- re-enfileira update se o usuário digitou durante o push
- sync no ON_RESUME; botão Sincronizar com spinner no lugar do ícone

Tempo real (SSE):
- SseClient (OkHttp, Dispatchers.IO, backoff) consome /api/events com Bearer
- saved: nota limpa recarrega sozinha (editor aberto incluído, com rekey por meta),
  nota suja mostra banner Recarregar/Manter; snackbar na lista
- presence: 👥N na lista/editor quando outra sessão (web ou app) abre a mesma nota
- SessionId único por instalação, enviado em presence e ecoado nos saves

Editor moderno (WebView nativo, sem deps novas):
- modos Editar / WYSIWYG / Preview; WYSIWYG = contenteditable com serializador
  html→md em JS na página-asset + ponte @JavascriptInterface (tudo na main thread)
- toolbar Markdown no modo texto (wrap/prefix seleção) e execCommand no WYSIWYG
- Preview renderiza md via /api/render; links não navegam fora do app
- lista: MediumTopAppBar, chips de filtro (categoria com caminho da árvore e tags),
  cards com status ☁/●/🗑/ e 👥, FAB estendido; telas em Material 3 dynamic color

Testado no emulador Android 15: título-only sync ✅, SSE ao vivo ✅, WYSIWYG→servidor ✅,
👥 presença ✅, conflito 409 com cópia única ✅
This commit is contained in:
lgcosta committed 2026-11-04 20:52:59 -03:00
1 parent 9e0bcc3e9b
commit 61d4abebbd
12 files changed
+1096 -154

No files matched your search

+37 -17
View File
@@ -90,20 +90,34 @@ notas-online-android/
rebaixadas (GET completo); notas que sumiram do servidor (e não estão na fila) são
removidas localmente.
5. **Gatilhos**: WorkManager periódico (1h, exige rede, backoff exponencial) · rede
restabelecida (`ConnectivityManager` callback) · após cada edição com conteúdo alterado ·
botão **Sincronizar** · troca de configuração.
6. **Autosave do editor**: grava local + enfileira após **10 s de ociosidade** (cada tecla
reinicia a contagem) — igual à política web.
restabelecida (`ConnectivityManager` callback) · **a cada edição com autosave (10 s de
ociosidade), incluindo mudanças só de título/formato/categorias** · **ao voltar para o app**
(`ON_RESUME`) · botão **Sincronizar** · troca de configuração. Uma auto-reconciliação a cada
rodada enfileira qualquer nota suja sem entrada na fila (recupera estados presos).
6. **Tempo real via SSE** (`GET /api/events`, OkHttp, reconexão com backoff, só em primeiro
plano): eventos `saved` atualizam a UI na hora — nota limpa aberta no editor é recarregada
ao vivo; nota suja mostra banner "Mudou no servidor — Recarregar/Manter"; snackbar na lista.
Eventos `presence` desenham **👥N** na nota quando outra sessão (app ou navegador web) está
com ela aberta.
7. **WYSIWYG e Preview** (WebView nativo, sem dependência extra): modo WYSIWYG renderiza o
Markdown (via `/api/render` do servidor) num `contenteditable`; cada edição é convertida de
volta para Markdown por um serializador JS embutido (títulos, negrito/itálico/tachado, listas,
citação, código, links, tabelas) e sincronizada — notas `.html` editam o HTML preservando
tags/atributos (scripts/`on*` sanitizados). Preview é o mesmo WebView não-editável. Barra de
ferramentas aplica `execCommand` (B I S H1 H2 listas ❝ 🔗 código).
## Telas
## UI (Material 3, dynamic color)
- **Servidor**: URL da API + token (`NOTES_TOKEN`) + autor; botão *Testar conexão* (lista
notas via `GET /api/notes`) antes de salvar.
- **Lista**: banner de status colorido — cinza *não configurado*, cinza-escuro *Offline (N na
fila)*, azul *Sincronizando…*, verde *Sincronizado às HH:mm · N pendente(s) · ⚠ conflitos*,
vermelho *erro/401*; itens mostram ● não enviado / 🗑 fila / ⚠ nota de conflito.
- **Editor**: título, chips de formato (TXT/MD/HTML), chips multi-seleção de categorias
(árvore espelhada do servidor), corpo monoespaçado; autosave ocioso + botão Salvar.
- **Lista**: `MediumTopAppBar`, banner de status colorido (Offline/Sincronizando/Sincronizado±
pendências/Erro), chips de filtro (Todas / 📁 categoria com caminho da árvore / #tag), cards
com título+extensão, tempo relativo, tags, chip de categoria(s) e ícone de status por nota
(☁✓ sincronizada, ● âmbar não enviada, 🗑 exclusão na fila, ⚠ conflito/mudança remota, 👥N
presença), FAB estendido "Nova".
- **Editor**: campo de título na top bar, seletor de modo (Editar / WYSIWYG / Preview), chips de
formato (TXT/MD/HTML) e de categorias (multi-seleção), corpo monoespaçado com toolbar de
Markdown (B I S H1 H2 • ❝ 🔗 ` { }) que envolve/prefixa a seleção, selo de presença 👥, e
horário do último salvamento.
- **Servidor**: URL + token + autor com *Testar conexão* e validação de esquema http(s).
## Compilar e instalar
@@ -128,16 +142,22 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
- criar nota offline → autosave por ociosidade (10 s) → `POST` único (sem duplicatas);
- editar offline → fila resiste à queda de rede → push automático ao restabelecer (`ConnectivityManager`);
- nota criada na Web aparece no device via pull;
- **mudança só de título sincroniza** (bug reportado em celular real, corrigido: metadados
entram na fila + auto-reconciliação + sync no `ON_RESUME`);
- **SSE ao vivo**: PUT vindo da Web atualiza o editor aberto em ~2 s, sem toque; snackbar na
lista; **👥 presença** aparece quando outra sessão abre a mesma nota;
- **WYSIWYG**: digitar no `contenteditable` → serialização html→md na ponte JS → autosave →
servidor recebe o texto; Preview renderiza o Markdown via `/api/render`;
- **conflito real**: device e Web editam a mesma nota → HTTP 409 (`base_hash`) → nota principal
adota o servidor e o texto do Android vira exatamente **uma** cópia "… (conflito)"
(idempotente; sincronizações serializadas por mutex + índices únicos na outbox).
- Integração com a API: mesmos contratos do web client (`base_hash`/409, categorias, revisões).
O servidor normaliza `categories`/`tags` vazios como `[]` (null quebrava clientes com
tipos não-nuláveis, ex. kotlinx.serialization).
- Integração com a API: mesmos contratos do web client (`base_hash`/409, categorias, revisões,
`/api/events`, `/api/presence`). O servidor normaliza `categories`/`tags` vazios como `[]`
(null quebrava clientes com tipos não-nuláveis, ex. kotlinx.serialization).
## Roadmap honesto (não implementado aqui)
- SSE (`/api/events`) no Android para push tempo-real em vez de gatilho pós-save (OkHttp SSE).
- Token em Keystore/EncryptedSharedPreferences (hoje: sandbox do app via DataStore).
- Editor WYSIWYG/realce de sintaxe mobile (fora do escopo "mais leve possível").
- Sync reativo incremental (o pull compara hashes por nota; milhares de notas pediriam um
endpoint de delta no servidor).
- Multi-perfil/servidores e sincronização de anexos.