Files
Docmost-App/app/src/main/java/com/docmost/app/ui/VersionViewerActivity.kt
Nino 922d31b7df v1.0.5: Fix history viewer - ContentConverter JsonElement type bug
- 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
2026-07-27 17:39:48 -06:00

274 lines
11 KiB
Kotlin

package com.docmost.app.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.docmost.app.R
import com.docmost.app.api.DocmostApi
import com.docmost.app.data.Credentials
import com.docmost.app.data.SessionManager
import com.docmost.app.utils.ContentConverter
import com.google.android.material.button.MaterialButton
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class VersionViewerActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var progress: CircularProgressIndicator
private lateinit var actionBar: LinearLayout
private lateinit var ivAvatar: ShapeableImageView
private lateinit var tvAuthor: TextView
private lateinit var tvDateTime: TextView
private lateinit var btnLoadVersion: MaterialButton
private lateinit var api: DocmostApi
private var creds: Credentials? = null
private var currentPageContent: String = ""
private var originalTipTapJson: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_version_viewer)
// Hide the default ActionBar
supportActionBar?.hide()
webView = findViewById(R.id.webViewVersion)
progress = findViewById(R.id.progressVersion)
actionBar = findViewById(R.id.actionBar)
ivAvatar = findViewById(R.id.ivAvatar)
tvAuthor = findViewById(R.id.tvAuthor)
tvDateTime = findViewById(R.id.tvDateTime)
btnLoadVersion = findViewById(R.id.btnLoadVersion)
val pageId = intent.getStringExtra("pageId") ?: return finish()
val historyId = intent.getStringExtra("historyId") ?: return finish()
val author = intent.getStringExtra("author") ?: "Desconocido"
val dateTime = intent.getStringExtra("dateTime") ?: ""
val sessionManager = SessionManager(this)
com.docmost.app.utils.ImageLoader.init(this, sessionManager)
creds = sessionManager.getCredentials()
if (creds == null) {
finish()
return
}
api = DocmostApi(creds!!.url)
api.setAuthToken(creds!!.authToken)
tvAuthor.text = author
tvDateTime.text = dateTime
// Cargar avatar del autor
CoroutineScope(Dispatchers.IO).launch {
try {
val history = api.getPageHistoryInfo(pageId, historyId)
val avatarUrl = history.lastUpdatedBy?.avatarUrl
if (!avatarUrl.isNullOrBlank()) {
withContext(Dispatchers.Main) {
val fullAvatarUrl = "${creds?.url}/api/attachments/img/avatar/$avatarUrl"
com.docmost.app.utils.ImageLoader.loadSync(fullAvatarUrl, ivAvatar, R.drawable.ic_page)
}
} else {
withContext(Dispatchers.Main) {
ivAvatar.setImageResource(R.drawable.ic_page)
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
ivAvatar.setImageResource(R.drawable.ic_page)
}
}
}
btnLoadVersion.setOnClickListener {
loadVersionInEditor(pageId, historyId)
}
setupWebView(pageId, historyId)
}
private fun setupWebView(pageId: String, historyId: String) {
val isDark = when (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) {
android.content.res.Configuration.UI_MODE_NIGHT_YES -> true
else -> false
}
CoroutineScope(Dispatchers.IO).launch {
try {
val history = api.getPageHistoryInfo(pageId, historyId)
val htmlContent: String
if (history.content != null) {
val content = history.content
if (content is String && content.trimStart().startsWith("<")) {
htmlContent = content
originalTipTapJson = ""
} else if (content is Map<*, *>) {
val gson = Gson()
originalTipTapJson = gson.toJson(content)
htmlContent = ContentConverter.contentToHtml(content)
} else if (content is String) {
originalTipTapJson = content
htmlContent = ContentConverter.tipTapToHtml(content)
} else {
htmlContent = "<p>Sin contenido</p>"
originalTipTapJson = ""
}
} else {
htmlContent = "<p>Sin contenido</p>"
originalTipTapJson = ""
}
withContext(Dispatchers.Main) {
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = false
allowContentAccess = false
builtInZoomControls = true
displayZoomControls = false
loadWithOverviewMode = true
useWideViewPort = true
}
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
progress.visibility = View.GONE
}
}
val darkModeCss = if (isDark) """
body {
background-color: #1a1a1a !important;
color: #e0e0e0 !important;
}
.version-banner {
background: #2d2d2d !important;
border-color: #404040 !important;
color: #e0e0e0 !important;
}
pre, code {
background: #2d2d2d !important;
color: #e0e0e0 !important;
}
blockquote {
border-left-color: #404040 !important;
color: #9ca3af !important;
}
th {
background: #2a2a2a !important;
}
td, th {
border-color: #404040 !important;
}
a {
color: #64b5f6 !important;
}
""" else ""
val fullHtml = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
padding: 16px;
line-height: 1.6;
color: #333;
}
.version-banner {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
color: #856404;
}
.content {
max-width: 800px;
margin: 0 auto;
}
p { margin: 8px 0; }
h1, h2, h3, h4, h5, h6 { margin: 16px 0 8px 0; }
ul, ol { padding-left: 20px; }
pre { background: #f4f4f4; padding: 12px; border-radius: 4px; overflow-x: auto; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
blockquote { border-left: 3px solid #ccc; padding-left: 12px; margin: 8px 0; color: #666; }
img { max-width: 100%; height: auto; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; }
th { background: #f4f4f4; }
a { color: #0066cc; }
$darkModeCss
</style>
</head>
<body>
<div class="version-banner">
<strong>Versión de solo lectura</strong>
</div>
<div class="content">
$htmlContent
</div>
</body>
</html>
""".trimIndent()
webView.loadDataWithBaseURL(
"${creds?.url}/",
fullHtml,
"text/html",
"UTF-8",
null
)
currentPageContent = htmlContent
// Mostrar barra de acción con info del usuario y botón
actionBar.visibility = View.VISIBLE
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
progress.visibility = View.GONE
webView.loadData(
"<html><body><h1>Error</h1><p>${e.message}</p></body></html>",
"text/html",
"UTF-8"
)
}
}
}
}
private fun loadVersionInEditor(pageId: String, historyId: String) {
val resultIntent = Intent()
resultIntent.putExtra("loadVersion", true)
resultIntent.putExtra("pageId", pageId)
resultIntent.putExtra("historyId", historyId)
resultIntent.putExtra("tipTapJson", originalTipTapJson)
setResult(RESULT_OK, resultIntent)
finish()
}
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
super.onConfigurationChanged(newConfig)
setupWebView(
intent.getStringExtra("pageId") ?: return,
intent.getStringExtra("historyId") ?: return
)
}
}