diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 98d286d..5b95bb0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.docmost.app" minSdk = 26 targetSdk = 34 - versionCode = 2 - versionName = "1.0.1" + versionCode = 4 + versionName = "1.0.3" } signingConfigs { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 17bc0e8..8d9fc6e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -2,5 +2,14 @@ -keepattributes Signature -keepattributes *Annotation* -keep class com.docmost.app.api.** { *; } +-keep class com.docmost.app.data.** { *; } -dontwarn okhttp3.** -dontwarn okio.** + +# Gson - preserve TypeToken anonymous subclasses and generic signatures +-keep class com.google.gson.** { *; } +-keep class * extends com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.TypeAdapter { *; } +-keepattributes Signature +-keepattributes RuntimeVisibleAnnotations +-keep class sun.misc.Unsafe { *; } diff --git a/app/src/main/java/com/docmost/app/api/DocmostApi.kt b/app/src/main/java/com/docmost/app/api/DocmostApi.kt index 5a2c297..6860aa7 100644 --- a/app/src/main/java/com/docmost/app/api/DocmostApi.kt +++ b/app/src/main/java/com/docmost/app/api/DocmostApi.kt @@ -3,7 +3,6 @@ package com.docmost.app.api import android.util.Log import com.docmost.app.BuildConfig import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import android.content.Context @@ -119,17 +118,55 @@ class DocmostApi(private val baseUrl: String) { suspend fun getSpaces(): List = withContext(Dispatchers.IO) { val json = post("/api/spaces") - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - val items = response.data?.items ?: emptyList() - items + if (BuildConfig.DEBUG) Log.d("DocmostAPI", "Spaces response: $json") + val element = com.google.gson.JsonParser.parseString(json) + if (element.isJsonArray) { + element.asJsonArray.map { gson.fromJson(it, Space::class.java) } + } else { + val obj = element.asJsonObject + val data = obj.get("data") + if (data != null && data.isJsonObject) { + val items = data.asJsonObject.get("items") + if (items != null && items.isJsonArray) { + items.asJsonArray.map { gson.fromJson(it, Space::class.java) } + } else emptyList() + } else if (data != null && data.isJsonArray) { + data.asJsonArray.map { gson.fromJson(it, Space::class.java) } + } else emptyList() + } } suspend fun getSidebarPages(spaceId: String, pageId: String? = null): List = withContext(Dispatchers.IO) { val json = post("/api/pages/sidebar-pages", SidebarPagesRequest(spaceId = spaceId, pageId = pageId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, Page::class.java) } + } + + private fun parseJsonArray(json: String, mapper: (com.google.gson.JsonElement) -> T): List { + val element = com.google.gson.JsonParser.parseString(json) + if (element.isJsonArray) { + return element.asJsonArray.map(mapper) + } + val obj = element.asJsonObject + val data = obj.get("data") + if (data != null && data.isJsonObject) { + val items = data.asJsonObject.get("items") + if (items != null && items.isJsonArray) { + return items.asJsonArray.map(mapper) + } + } else if (data != null && data.isJsonArray) { + return data.asJsonArray.map(mapper) + } + return emptyList() + } + + private fun parseJsonObject(json: String, mapper: (String) -> T): T? { + val element = com.google.gson.JsonParser.parseString(json) + val objJson = if (element.isJsonObject) { + val obj = element.asJsonObject + val data = obj.get("data") + if (data != null && data.isJsonObject) data.toString() else json + } else json + return mapper(objJson) } private suspend fun fetchPageChildren(spaceId: String, parentPage: Page, allPages: MutableList) { @@ -156,9 +193,16 @@ class DocmostApi(private val baseUrl: String) { suspend fun getPageInfo(pageId: String, format: String = "json"): Page = withContext(Dispatchers.IO) { val json = post("/api/pages/info", PageInfoRequest(pageId, format)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Page not found in response") + Log.d("DocmostAPI", "PageInfo response: $json") + val element = com.google.gson.JsonParser.parseString(json) + val pageJson = if (element.isJsonObject) { + val obj = element.asJsonObject + val data = obj.get("data") + if (data != null && data.isJsonObject) data.toString() else json + } else json + Log.d("DocmostAPI", "PageInfo parsed: $pageJson") + val page = gson.fromJson(pageJson, Page::class.java) + page ?: throw IOException("Page not found in response") } suspend fun getPageHtml(pageId: String): String = withContext(Dispatchers.IO) { @@ -181,9 +225,8 @@ class DocmostApi(private val baseUrl: String) { description = description ) ) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Space update response missing data") + parseJsonObject(json) { gson.fromJson(it, Space::class.java) } + ?: throw IOException("Space update response missing data") } suspend fun uploadSpaceIcon(spaceId: String, imageUri: Uri, context: Context): String = @@ -235,38 +278,32 @@ class DocmostApi(private val baseUrl: String) { operation = "replace" ) ) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Update response missing data") + parseJsonObject(json) { gson.fromJson(it, Page::class.java) } + ?: throw IOException("Update response missing data") } suspend fun getCurrentUser(): User = withContext(Dispatchers.IO) { val json = post("/api/users/me") - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.user ?: throw IOException("User not found in response") + val user = parseJsonObject(json) { gson.fromJson(it, User::class.java) } + user ?: throw IOException("User not found in response") } suspend fun updateUser(name: String?): User = withContext(Dispatchers.IO) { val json = post("/api/users/update", UserUpdateRequest(name = name)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Update user response missing data") + parseJsonObject(json) { gson.fromJson(it, User::class.java) } + ?: throw IOException("Update user response missing data") } suspend fun searchPages(query: String): List = withContext(Dispatchers.IO) { val json = post("/api/search", SearchRequest(query = query)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, SearchResult::class.java) } } suspend fun createSpace(name: String, slug: String, description: String? = null): Space = withContext(Dispatchers.IO) { val json = post("/api/spaces/create", CreateSpaceRequest(name = name, slug = slug, description = description)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Create space response missing data") + parseJsonObject(json) { gson.fromJson(it, Space::class.java) } + ?: throw IOException("Create space response missing data") } suspend fun removeAvatar() = withContext(Dispatchers.IO) { @@ -306,16 +343,13 @@ class DocmostApi(private val baseUrl: String) { suspend fun getRecentPages(): List = withContext(Dispatchers.IO) { val json = post("/api/pages/recent", RecentPageRequest()) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, Page::class.java) } } suspend fun createPage(title: String, spaceId: String): Page = withContext(Dispatchers.IO) { val json = post("/api/pages/create", CreatePageRequest(title = title, spaceId = spaceId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Create page response missing data") + parseJsonObject(json) { gson.fromJson(it, Page::class.java) } + ?: throw IOException("Create page response missing data") } suspend fun deletePage(pageId: String) = withContext(Dispatchers.IO) { @@ -328,16 +362,13 @@ class DocmostApi(private val baseUrl: String) { suspend fun getShares(): List = withContext(Dispatchers.IO) { val json = post("/api/shares/", mapOf("limit" to 50)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, Share::class.java) } } suspend fun createShare(pageId: String): Share = withContext(Dispatchers.IO) { val json = post("/api/shares/create", CreateShareDto(pageId = pageId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Create share response missing data") + parseJsonObject(json) { gson.fromJson(it, Share::class.java) } + ?: throw IOException("Create share response missing data") } suspend fun deleteShare(shareId: String) = withContext(Dispatchers.IO) { @@ -346,23 +377,19 @@ class DocmostApi(private val baseUrl: String) { suspend fun getComments(pageId: String): List = withContext(Dispatchers.IO) { val json = post("/api/comments/", mapOf("pageId" to pageId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, Comment::class.java) } } suspend fun createComment(pageId: String, content: String): Comment = withContext(Dispatchers.IO) { val json = post("/api/comments/create", CreateCommentDto(pageId = pageId, content = content)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Create comment response missing data") + parseJsonObject(json) { gson.fromJson(it, Comment::class.java) } + ?: throw IOException("Create comment response missing data") } suspend fun updateComment(commentId: String, content: String): Comment = withContext(Dispatchers.IO) { val json = post("/api/comments/update", UpdateCommentDto(commentId = commentId, content = content)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Update comment response missing data") + parseJsonObject(json) { gson.fromJson(it, Comment::class.java) } + ?: throw IOException("Update comment response missing data") } suspend fun deleteComment(commentId: String) = withContext(Dispatchers.IO) { @@ -371,9 +398,8 @@ class DocmostApi(private val baseUrl: String) { suspend fun duplicatePage(pageId: String, targetSpaceId: String? = null): Page = withContext(Dispatchers.IO) { val json = post("/api/pages/duplicate", DuplicatePageRequest(pageId = pageId, spaceId = targetSpaceId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Duplicate response missing data") + parseJsonObject(json) { gson.fromJson(it, Page::class.java) } + ?: throw IOException("Duplicate response missing data") } suspend fun movePageToSpace(pageId: String, spaceId: String) = withContext(Dispatchers.IO) { @@ -391,31 +417,33 @@ class DocmostApi(private val baseUrl: String) { suspend fun getPageHistory(pageId: String): List = withContext(Dispatchers.IO) { val json = post("/api/pages/history", PageHistoryRequest(pageId = pageId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, PageHistory::class.java) } } suspend fun getPageHistoryInfo(pageId: String, historyId: String): PageHistory = withContext(Dispatchers.IO) { val json = post("/api/pages/history/info", mapOf("pageId" to pageId, "historyId" to historyId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data ?: throw IOException("Page history not found in response") + parseJsonObject(json) { gson.fromJson(it, PageHistory::class.java) } + ?: throw IOException("Page history not found in response") } suspend fun getPageContent(pageId: String): PageContent = withContext(Dispatchers.IO) { val json = post("/api/pages/info", PageInfoRequest(pageId = pageId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - val page = response.data ?: throw IOException("Page not found in response") + Log.d("DocmostAPI", "PageContent response: $json") + val element = com.google.gson.JsonParser.parseString(json) + val pageJson = if (element.isJsonObject) { + val obj = element.asJsonObject + val data = obj.get("data") + if (data != null && data.isJsonObject) data.toString() else json + } else json + Log.d("DocmostAPI", "PageContent parsed: $pageJson") + val page = gson.fromJson(pageJson, Page::class.java) + page ?: throw IOException("Page not found in response") PageContent(source = page.source ?: "") } suspend fun getDeletedPages(spaceId: String): List = withContext(Dispatchers.IO) { val json = post("/api/pages/trash", DeletedPagesRequest(spaceId = spaceId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, DeletedPage::class.java) } } suspend fun restorePage(pageId: String, spaceId: String? = null) = withContext(Dispatchers.IO) { @@ -467,9 +495,7 @@ class DocmostApi(private val baseUrl: String) { suspend fun getPageAttachments(pageId: String): List = withContext(Dispatchers.IO) { val json = post("/api/attachments/page", mapOf("pageId" to pageId)) - val type = object : TypeToken>() {}.type - val response = gson.fromJson>(json, type) - response.data?.items ?: emptyList() + parseJsonArray(json) { gson.fromJson(it, Attachment::class.java) } } private fun getFileNameFromUri(context: android.content.Context, uri: android.net.Uri): String? { diff --git a/app/src/main/java/com/docmost/app/data/PageCacheManager.kt b/app/src/main/java/com/docmost/app/data/PageCacheManager.kt index dbf700c..45aff52 100644 --- a/app/src/main/java/com/docmost/app/data/PageCacheManager.kt +++ b/app/src/main/java/com/docmost/app/data/PageCacheManager.kt @@ -4,7 +4,7 @@ import android.content.Context import com.docmost.app.api.Page import com.docmost.app.BuildConfig import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import com.google.gson.JsonParser import java.io.File import java.security.MessageDigest @@ -123,8 +123,8 @@ class PageCacheManager(private val context: Context) { fun getCachedPages(): List { return try { if (!indexFile.exists()) return emptyList() - val type = object : TypeToken>() {}.type - gson.fromJson(indexFile.readText(), type) ?: emptyList() + val array = JsonParser.parseString(indexFile.readText()).asJsonArray + array.map { gson.fromJson(it, CachedPageMeta::class.java) } } catch (e: Exception) { emptyList() } @@ -153,8 +153,8 @@ class PageCacheManager(private val context: Context) { return try { val file = File(spacePagesDir, "$spaceId.json") if (!file.exists()) return null - val type = object : TypeToken>() {}.type - gson.fromJson(file.readText(), type) + val array = JsonParser.parseString(file.readText()).asJsonArray + array.map { gson.fromJson(it, Page::class.java) } } catch (e: Exception) { null } @@ -178,8 +178,8 @@ class PageCacheManager(private val context: Context) { private fun getIndex(): List { return try { if (!indexFile.exists()) return emptyList() - val type = object : TypeToken>() {}.type - gson.fromJson(indexFile.readText(), type) ?: emptyList() + val array = JsonParser.parseString(indexFile.readText()).asJsonArray + array.map { gson.fromJson(it, CachedPageMeta::class.java) } } catch (e: Exception) { emptyList() } diff --git a/app/src/main/java/com/docmost/app/data/SessionManager.kt b/app/src/main/java/com/docmost/app/data/SessionManager.kt index 96338dd..a0aae75 100644 --- a/app/src/main/java/com/docmost/app/data/SessionManager.kt +++ b/app/src/main/java/com/docmost/app/data/SessionManager.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys import com.google.gson.Gson +import com.google.gson.JsonParser data class Credentials( @@ -110,8 +111,8 @@ class SessionManager(private val context: Context) { null } ?: return emptyList() return try { - val type = com.google.gson.reflect.TypeToken.getParameterized(List::class.java, Credentials::class.java).type - gson.fromJson(json, type) + val array = JsonParser.parseString(json).asJsonArray + array.map { gson.fromJson(it, Credentials::class.java) } } catch (e: Exception) { emptyList() } diff --git a/app/src/main/java/com/docmost/app/data/SpaceCacheManager.kt b/app/src/main/java/com/docmost/app/data/SpaceCacheManager.kt index e8e4994..c951415 100644 --- a/app/src/main/java/com/docmost/app/data/SpaceCacheManager.kt +++ b/app/src/main/java/com/docmost/app/data/SpaceCacheManager.kt @@ -4,7 +4,7 @@ import android.content.Context import com.docmost.app.api.Space import com.docmost.app.BuildConfig import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import com.google.gson.JsonParser import java.io.File import java.security.MessageDigest @@ -119,8 +119,8 @@ class SpaceCacheManager(private val context: Context) { fun getIndex(): List { return try { if (!indexFile.exists()) return emptyList() - val type = object : TypeToken>() {}.type - gson.fromJson(indexFile.readText(), type) ?: emptyList() + val array = JsonParser.parseString(indexFile.readText()).asJsonArray + array.map { gson.fromJson(it, CachedSpaceMeta::class.java) } } catch (e: Exception) { emptyList() } diff --git a/app/src/main/java/com/docmost/app/data/SyncQueue.kt b/app/src/main/java/com/docmost/app/data/SyncQueue.kt index cefbaae..5467de4 100644 --- a/app/src/main/java/com/docmost/app/data/SyncQueue.kt +++ b/app/src/main/java/com/docmost/app/data/SyncQueue.kt @@ -6,7 +6,6 @@ import androidx.room.PrimaryKey import androidx.room.TypeConverter import androidx.room.TypeConverters import com.google.gson.Gson -import com.google.gson.reflect.TypeToken enum class OperationType { CREATE_SPACE, diff --git a/app/src/main/java/com/docmost/app/ui/PageEditorActivity.kt b/app/src/main/java/com/docmost/app/ui/PageEditorActivity.kt index 486ce57..50495dc 100644 --- a/app/src/main/java/com/docmost/app/ui/PageEditorActivity.kt +++ b/app/src/main/java/com/docmost/app/ui/PageEditorActivity.kt @@ -59,7 +59,6 @@ import com.docmost.app.utils.NetworkConnectivityObserver import com.docmost.app.utils.NetworkStatus import com.docmost.app.databinding.ActivityPageEditorBinding import com.google.gson.GsonBuilder -import com.google.gson.reflect.TypeToken import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -1878,9 +1877,9 @@ class PageEditorActivity : AppCompatActivity() { private fun tiptapToMarkdown(tiptapJson: String): String { return try { - val mapType = object : TypeToken>() {}.type - val doc = gson.fromJson>(tiptapJson, mapType) - extractText(doc) + val doc = gson.fromJson(tiptapJson, com.google.gson.JsonObject::class.java).asMap() + @Suppress("UNCHECKED_CAST") + extractText(doc as Map) } catch (e: Exception) { tiptapJson } @@ -2090,11 +2089,13 @@ class PageEditorActivity : AppCompatActivity() { canEdit = page.permissions?.canEdit ?: true currentPage = page + android.util.Log.d("PageEditor", "renderPageContent: page.content type=${page.content?.let { it::class.java.simpleName } ?: "null"}") + if (!canEdit) { loadReadOnlyPreview(page) } else { val markdown = contentToMarkdown(page.content) - if (BuildConfig.DEBUG) android.util.Log.d("PageEditor", "Markdown content length: ${markdown.length}") + android.util.Log.d("PageEditor", "Markdown content length: ${markdown.length}") currentPageContent = markdown originalContent = markdown binding.etTitle.setText(page.title) @@ -2484,15 +2485,59 @@ class PageEditorActivity : AppCompatActivity() { private fun contentToMarkdown(content: Any?): String { if (content == null) return "" - if (content is String) return content + if (content is String) { + return if (content.trimStart().startsWith("<")) { + htmlToMarkdown(content) + } else { + content + } + } return try { val json = gson.toJson(content) - tiptapToMarkdown(json) + if (json.isNotEmpty() && json != "null") { + tiptapToMarkdown(json) + } else { + "" + } } catch (e: Exception) { - content.toString() + android.util.Log.e("PageEditor", "contentToMarkdown error: ${e.message}") + "" } } + private fun htmlToMarkdown(html: String): String { + return html + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "# ${it.groupValues[1].trim()}\n\n" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "## ${it.groupValues[1].trim()}\n\n" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "### ${it.groupValues[1].trim()}\n\n" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "#### ${it.groupValues[1].trim()}\n\n" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "##### ${it.groupValues[1].trim()}\n\n" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "###### ${it.groupValues[1].trim()}\n\n" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "**${it.groupValues[1]}**" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "**${it.groupValues[1]}**" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "*${it.groupValues[1]}*" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "*${it.groupValues[1]}*" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "~~${it.groupValues[1]}~~" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "`${it.groupValues[1]}`" } + .replace(Regex("]*href=\"([^\"]+)\"[^>]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "[${it.groupValues[2]}](${it.groupValues[1]})" } + .replace(Regex("]*src=\"([^\"]+)\"[^>]*alt=\"([^\"]*)\"[^>]*/?>", RegexOption.DOT_MATCHES_ALL)) { "![${it.groupValues[2]}](${it.groupValues[1]})" } + .replace(Regex("]*alt=\"([^\"]*)\"[^>]*src=\"([^\"]+)\"[^>]*/?>", RegexOption.DOT_MATCHES_ALL)) { "![${it.groupValues[1]}](${it.groupValues[2]})" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "- ${it.groupValues[1].replace(Regex("<[^>]*>"), "").trim()}\n" } + .replace(Regex("]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "> ${it.groupValues[1].replace(Regex("<[^>]*>"), "").trim()}\n\n" } + .replace(Regex("]*>]*>(.*?)", RegexOption.DOT_MATCHES_ALL)) { "```\n${it.groupValues[1].replace(Regex("<[^>]*>"), "")}\n```\n\n" } + .replace(Regex("]*>(.*?)

", RegexOption.DOT_MATCHES_ALL)) { "${it.groupValues[1].replace(Regex("<[^>]*>"), "").trim()}\n\n" } + .replace(Regex("]*/?>", RegexOption.DOT_MATCHES_ALL)) { "\n" } + .replace(Regex("]*/?>", RegexOption.DOT_MATCHES_ALL)) { "---\n\n" } + .replace(Regex("<[^>]*>"), "") + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace(" ", " ") + .replace(Regex("\n{3,}"), "\n\n") + .trim() + } + private fun markdownToTipTap(markdown: String): String { // This function converts Markdown to TipTap JSON format that Docmost expects // It handles attachments specially to preserve the attachment type diff --git a/app/src/main/java/com/docmost/app/ui/SpacesActivity.kt b/app/src/main/java/com/docmost/app/ui/SpacesActivity.kt index fa82246..eb39d0a 100644 --- a/app/src/main/java/com/docmost/app/ui/SpacesActivity.kt +++ b/app/src/main/java/com/docmost/app/ui/SpacesActivity.kt @@ -54,7 +54,6 @@ import com.docmost.app.utils.NetworkConnectivityObserver import com.docmost.app.utils.NetworkStatus import com.docmost.app.databinding.ActivitySpacesBinding import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayout @@ -909,8 +908,8 @@ class SpacesActivity : AppCompatActivity() { private fun loadRecentCache(): List? { try { val json = prefs.getString("recent_cache", null) ?: return null - val type = object : TypeToken>() {}.type - return gson.fromJson(json, type) + val array = com.google.gson.JsonParser.parseString(json).asJsonArray + return array.map { gson.fromJson(it, Page::class.java) } } catch (_: Exception) { return null } } diff --git a/app/src/main/java/com/docmost/app/utils/ContentConverter.kt b/app/src/main/java/com/docmost/app/utils/ContentConverter.kt index df373ee..01abd69 100644 --- a/app/src/main/java/com/docmost/app/utils/ContentConverter.kt +++ b/app/src/main/java/com/docmost/app/utils/ContentConverter.kt @@ -1,16 +1,16 @@ package com.docmost.app.utils import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import com.google.gson.JsonObject object ContentConverter { private val gson = Gson() fun tipTapJsonToHtml(tipTapJson: String): String { return try { - val mapType = object : TypeToken>() {}.type - val doc = gson.fromJson>(tipTapJson, mapType) - nodeToHtml(doc) + val doc = gson.fromJson(tipTapJson, JsonObject::class.java).asMap() + @Suppress("UNCHECKED_CAST") + nodeToHtml(doc as Map) } catch (e: Exception) { tipTapJson } @@ -18,9 +18,9 @@ object ContentConverter { fun tipTapToHtml(tipTapJson: String): String { return try { - val mapType = object : TypeToken>() {}.type - val doc = gson.fromJson>(tipTapJson, mapType) - nodeToHtml(doc) + val doc = gson.fromJson(tipTapJson, JsonObject::class.java).asMap() + @Suppress("UNCHECKED_CAST") + nodeToHtml(doc as Map) } catch (e: Exception) { tipTapJson } diff --git a/settings.gradle.kts b/settings.gradle.kts index f4343dc..5b12ea5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,3 +14,12 @@ dependencyResolutionManagement { rootProject.name = "Docmost" include(":app") + +val localBuildDir = "/tmp/android-build/Docmost" + +gradle.projectsLoaded { + rootProject.layout.buildDirectory.set(file(localBuildDir)) + rootProject.subprojects.forEach { project -> + project.layout.buildDirectory.set(file("$localBuildDir/${project.name}")) + } +}