Root cause chain:
1. JS _pendingSet cleared prematurely in onReady (0ms), letting onChange
fire during initial content load and set userEdited=true
2. onAutoSave didn't check isEditorReadOnly, firing even for read-only pages
3. Markdown round-trip (extractText → markdownToBlocks → blocksToMarkdown)
produced different output, triggering false change detection
4. onSaveContent passed null for title when no title changes, which
CachedApi converted to empty string, causing server to set title to 'untitled'
Fixes:
- JS: Remove premature _pendingSet clearing in onReady; increase timeout to 500ms
- Kotlin: onAutoSave checks isEditorReadOnly (no auto-save in read-only mode)
- Kotlin: Save operations always pass current title (never null) to prevent
empty title being sent to server
- Kotlin: Add isContentLoading flag to suppress onContentChanged during initial
content load, preventing false currentPageContent updates
- Added APK versioned naming for Obtanium compatibility
- Sync All button: downloads all pages from all spaces recursively
- Skips already-cached pages (checks updatedAt to avoid re-downloading)
- Progress dialog shows space/page counter during sync
- Cancel support via dialog cancel button
- Clean Cache button: removes all downloaded pages with confirmation
- Preserves local drafts when cleaning cache
- Added to Settings > Offline section
- ContentConverter.tipTapToHtml() had same JsonObject.asMap() bug as tiptapToMarkdown
- All as? String casts silently returned null, producing empty HTML
- Now uses Gson TypeToken<Map<String, Any?>> for proper deserialization
- Added contentToHtml() that handles String (HTML), Map (TipTap), and JSON string
- Fixed heading level cast (Gson deserializes numbers as Double, not Int)
- VersionViewerActivity handles all content types correctly
- tiptapToMarkdown used JsonObject.asMap() which returns Map<String, JsonElement>
- All 'as? String' casts silently failed (JsonPrimitive != String), returning empty
- Now uses Gson TypeToken<Map<String, Any?>> for proper raw type deserialization
- contentToMarkdown now handles Map types directly without serialize roundtrip
- Replace all TypeToken usage with JsonParser/gson.fromJson (fixes R8/ProGuard crash)
- Add parseJsonArray/parseJsonObject helpers for API response parsing
- Fix contentToMarkdown to properly convert TipTap JSON objects to Markdown
using tiptapToMarkdown instead of returning raw JSON
- Fix API endpoints to handle unwrapped response formats
- Add ProGuard rules for Gson TypeToken preservation
- Redirect build dir to /tmp for Samba compatibility