Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f265498ed3 | |||
| 57f8858708 | |||
| 922d31b7df | |||
| ae57a0bf09 | |||
| 7be826f9ba | |||
| bc6ed08648 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -55,3 +55,7 @@ DocmostApp.md
|
|||||||
|
|
||||||
# App build
|
# App build
|
||||||
app/build/
|
app/build/
|
||||||
|
|
||||||
|
# Signing
|
||||||
|
*.jks
|
||||||
|
keystore.properties
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(keystorePropertiesFile.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.docmost.app"
|
namespace = "com.docmost.app"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
@@ -12,14 +20,24 @@ android {
|
|||||||
applicationId = "com.docmost.app"
|
applicationId = "com.docmost.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 8
|
||||||
versionName = "1.0"
|
versionName = "1.0.7"
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(keystoreProperties.getProperty("storeFile", "release.jks"))
|
||||||
|
storePassword = keystoreProperties.getProperty("storePassword", "")
|
||||||
|
keyAlias = keystoreProperties.getProperty("keyAlias", "")
|
||||||
|
keyPassword = keystoreProperties.getProperty("keyPassword", "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@@ -38,6 +56,14 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all {
|
||||||
|
outputs.all {
|
||||||
|
val output = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||||
|
output.outputFileName = "Docmost-app-v${versionName}-${buildType.name}.apk"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
app/proguard-rules.pro
vendored
9
app/proguard-rules.pro
vendored
@@ -2,5 +2,14 @@
|
|||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
-keepattributes *Annotation*
|
-keepattributes *Annotation*
|
||||||
-keep class com.docmost.app.api.** { *; }
|
-keep class com.docmost.app.api.** { *; }
|
||||||
|
-keep class com.docmost.app.data.** { *; }
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-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 { *; }
|
||||||
|
|||||||
@@ -531,12 +531,9 @@
|
|||||||
} else {
|
} else {
|
||||||
console.log('AndroidInterface not available yet');
|
console.log('AndroidInterface not available yet');
|
||||||
}
|
}
|
||||||
// Android controla la carga de contenido via setContent()
|
// _pendingSet is cleared by setContent() after 100ms
|
||||||
// No cargar pendingAndroidContent automáticamente
|
// Do NOT clear it here — doing so prematurely lets onChange
|
||||||
// Clear pending set flag on next tick after initialization
|
// fire during initial content load and sets userEdited=true
|
||||||
setTimeout(function() {
|
|
||||||
window._pendingSet = false;
|
|
||||||
}, 0);
|
|
||||||
// Configurar observer para detectar cambios en bloques vacíos
|
// Configurar observer para detectar cambios en bloques vacíos
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
setupBackspaceHandler();
|
setupBackspaceHandler();
|
||||||
@@ -617,7 +614,7 @@
|
|||||||
window.setContentLock = false;
|
window.setContentLock = false;
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
window._pendingSet = false;
|
window._pendingSet = false;
|
||||||
}, 100);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.getContent = function() {
|
window.getContent = function() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.docmost.app.api
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.docmost.app.BuildConfig
|
||||||
import com.docmost.app.utils.NetworkConnectivityObserver
|
import com.docmost.app.utils.NetworkConnectivityObserver
|
||||||
import com.docmost.app.data.PageCacheManager
|
import com.docmost.app.data.PageCacheManager
|
||||||
import com.docmost.app.data.SpaceCacheManager
|
import com.docmost.app.data.SpaceCacheManager
|
||||||
@@ -18,10 +19,23 @@ class CachedApi(
|
|||||||
private val networkObserver: NetworkConnectivityObserver
|
private val networkObserver: NetworkConnectivityObserver
|
||||||
) {
|
) {
|
||||||
var forcedOffline: Boolean = false
|
var forcedOffline: Boolean = false
|
||||||
|
var isSyncing: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
val isOffline: Boolean
|
val isOffline: Boolean
|
||||||
get() = forcedOffline || networkObserver.isOffline()
|
get() = forcedOffline || networkObserver.isOffline()
|
||||||
|
|
||||||
|
data class SyncProgress(
|
||||||
|
val spaceName: String,
|
||||||
|
val spaceIndex: Int,
|
||||||
|
val totalSpaces: Int,
|
||||||
|
val pageTitle: String,
|
||||||
|
val pageCount: Int,
|
||||||
|
val totalPages: Int,
|
||||||
|
val skipped: Int,
|
||||||
|
val errors: Int
|
||||||
|
)
|
||||||
|
|
||||||
private fun <T> cacheOrThrow(block: () -> T): T {
|
private fun <T> cacheOrThrow(block: () -> T): T {
|
||||||
return try {
|
return try {
|
||||||
block()
|
block()
|
||||||
@@ -365,4 +379,96 @@ class CachedApi(
|
|||||||
}
|
}
|
||||||
return fileName ?: uri.lastPathSegment
|
return fileName ?: uri.lastPathSegment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sync All ──
|
||||||
|
|
||||||
|
suspend fun syncAllPages(onProgress: (SyncProgress) -> Unit): Pair<Int, Int> {
|
||||||
|
if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión")
|
||||||
|
if (isSyncing) throw IllegalStateException("Sync already in progress")
|
||||||
|
isSyncing = true
|
||||||
|
var skipped = 0
|
||||||
|
var errors = 0
|
||||||
|
var totalSynced = 0
|
||||||
|
try {
|
||||||
|
val spaces = api.getSpaces()
|
||||||
|
spaceCache.cacheSpaces(spaces)
|
||||||
|
val allRootPages = mutableMapOf<String, List<Page>>()
|
||||||
|
|
||||||
|
for ((spaceIdx, space) in spaces.withIndex()) {
|
||||||
|
try {
|
||||||
|
val rootPages = api.getSidebarPages(space.id)
|
||||||
|
allRootPages[space.id] = rootPages
|
||||||
|
pageCache.cachePagesForSpace(space.id, rootPages)
|
||||||
|
|
||||||
|
val allPageIds = mutableListOf<Page>()
|
||||||
|
allPageIds.addAll(rootPages)
|
||||||
|
rootPages.forEach { page ->
|
||||||
|
if (page.hasChildren) {
|
||||||
|
val children = fetchAllChildren(space.id, page)
|
||||||
|
allPageIds.addAll(children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageIdx = 0
|
||||||
|
for (page in allPageIds) {
|
||||||
|
if (!isSyncing) break
|
||||||
|
pageIdx++
|
||||||
|
onProgress(SyncProgress(
|
||||||
|
spaceName = space.name,
|
||||||
|
spaceIndex = spaceIdx + 1,
|
||||||
|
totalSpaces = spaces.size,
|
||||||
|
pageTitle = page.title,
|
||||||
|
pageCount = pageIdx,
|
||||||
|
totalPages = allPageIds.size,
|
||||||
|
skipped = skipped,
|
||||||
|
errors = errors
|
||||||
|
))
|
||||||
|
try {
|
||||||
|
val cached = pageCache.getCachedPage(page.id)
|
||||||
|
if (cached != null && cached.updatedAt == page.updatedAt) {
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val fetchedPage = api.getPageInfo(page.id)
|
||||||
|
pageCache.cachePage(fetchedPage)
|
||||||
|
totalSynced++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
if (BuildConfig.DEBUG) Log.e("CachedApi", "Sync page error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
if (BuildConfig.DEBUG) Log.e("CachedApi", "Sync space error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isSyncing = false
|
||||||
|
}
|
||||||
|
return Pair(totalSynced, errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchAllChildren(spaceId: String, parentPage: Page): List<Page> {
|
||||||
|
val children = mutableListOf<Page>()
|
||||||
|
try {
|
||||||
|
val childPages = api.getSidebarPages(spaceId, parentPage.id)
|
||||||
|
children.addAll(childPages)
|
||||||
|
childPages.forEach { child ->
|
||||||
|
if (child.hasChildren) {
|
||||||
|
children.addAll(fetchAllChildren(spaceId, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopSync() {
|
||||||
|
isSyncing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAllCachedPages(): Int {
|
||||||
|
return pageCache.clearAllCachedPages()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachedPageCount(): Int = pageCache.getCachedPageCount()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.docmost.app.api
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.docmost.app.BuildConfig
|
import com.docmost.app.BuildConfig
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -119,17 +118,55 @@ class DocmostApi(private val baseUrl: String) {
|
|||||||
|
|
||||||
suspend fun getSpaces(): List<Space> = withContext(Dispatchers.IO) {
|
suspend fun getSpaces(): List<Space> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/spaces")
|
val json = post("/api/spaces")
|
||||||
val type = object : TypeToken<DocmostResponse<SpacesData>>() {}.type
|
if (BuildConfig.DEBUG) Log.d("DocmostAPI", "Spaces response: $json")
|
||||||
val response = gson.fromJson<DocmostResponse<SpacesData>>(json, type)
|
val element = com.google.gson.JsonParser.parseString(json)
|
||||||
val items = response.data?.items ?: emptyList()
|
if (element.isJsonArray) {
|
||||||
items
|
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<Page> = withContext(Dispatchers.IO) {
|
suspend fun getSidebarPages(spaceId: String, pageId: String? = null): List<Page> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/sidebar-pages", SidebarPagesRequest(spaceId = spaceId, pageId = pageId))
|
val json = post("/api/pages/sidebar-pages", SidebarPagesRequest(spaceId = spaceId, pageId = pageId))
|
||||||
val type = object : TypeToken<DocmostResponse<PagesData>>() {}.type
|
parseJsonArray<Page>(json) { gson.fromJson(it, Page::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<PagesData>>(json, type)
|
}
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
|
private fun <T> parseJsonArray(json: String, mapper: (com.google.gson.JsonElement) -> T): List<T> {
|
||||||
|
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 <T> 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<Page>) {
|
private suspend fun fetchPageChildren(spaceId: String, parentPage: Page, allPages: MutableList<Page>) {
|
||||||
@@ -156,9 +193,16 @@ class DocmostApi(private val baseUrl: String) {
|
|||||||
|
|
||||||
suspend fun getPageInfo(pageId: String, format: String = "json"): Page = withContext(Dispatchers.IO) {
|
suspend fun getPageInfo(pageId: String, format: String = "json"): Page = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/info", PageInfoRequest(pageId, format))
|
val json = post("/api/pages/info", PageInfoRequest(pageId, format))
|
||||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
Log.d("DocmostAPI", "PageInfo response: $json")
|
||||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
val element = com.google.gson.JsonParser.parseString(json)
|
||||||
response.data ?: throw IOException("Page not found in response")
|
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) {
|
suspend fun getPageHtml(pageId: String): String = withContext(Dispatchers.IO) {
|
||||||
@@ -181,9 +225,8 @@ class DocmostApi(private val baseUrl: String) {
|
|||||||
description = description
|
description = description
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val type = object : TypeToken<DocmostResponse<Space>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Space::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Space>>(json, type)
|
?: throw IOException("Space update response missing data")
|
||||||
response.data ?: throw IOException("Space update response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun uploadSpaceIcon(spaceId: String, imageUri: Uri, context: Context): String =
|
suspend fun uploadSpaceIcon(spaceId: String, imageUri: Uri, context: Context): String =
|
||||||
@@ -235,38 +278,32 @@ class DocmostApi(private val baseUrl: String) {
|
|||||||
operation = "replace"
|
operation = "replace"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Page::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
?: throw IOException("Update response missing data")
|
||||||
response.data ?: throw IOException("Update response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCurrentUser(): User = withContext(Dispatchers.IO) {
|
suspend fun getCurrentUser(): User = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/users/me")
|
val json = post("/api/users/me")
|
||||||
val type = object : TypeToken<DocmostResponse<UserInfoResponse>>() {}.type
|
val user = parseJsonObject(json) { gson.fromJson(it, User::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<UserInfoResponse>>(json, type)
|
user ?: throw IOException("User not found in response")
|
||||||
response.data?.user ?: throw IOException("User not found in response")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateUser(name: String?): User = withContext(Dispatchers.IO) {
|
suspend fun updateUser(name: String?): User = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/users/update", UserUpdateRequest(name = name))
|
val json = post("/api/users/update", UserUpdateRequest(name = name))
|
||||||
val type = object : TypeToken<DocmostResponse<User>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, User::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<User>>(json, type)
|
?: throw IOException("Update user response missing data")
|
||||||
response.data ?: throw IOException("Update user response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchPages(query: String): List<SearchResult> = withContext(Dispatchers.IO) {
|
suspend fun searchPages(query: String): List<SearchResult> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/search", SearchRequest(query = query))
|
val json = post("/api/search", SearchRequest(query = query))
|
||||||
val type = object : TypeToken<DocmostResponse<SearchResultsResponse>>() {}.type
|
parseJsonArray(json) { gson.fromJson(it, SearchResult::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<SearchResultsResponse>>(json, type)
|
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createSpace(name: String, slug: String, description: String? = null): Space =
|
suspend fun createSpace(name: String, slug: String, description: String? = null): Space =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/spaces/create", CreateSpaceRequest(name = name, slug = slug, description = description))
|
val json = post("/api/spaces/create", CreateSpaceRequest(name = name, slug = slug, description = description))
|
||||||
val type = object : TypeToken<DocmostResponse<Space>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Space::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Space>>(json, type)
|
?: throw IOException("Create space response missing data")
|
||||||
response.data ?: throw IOException("Create space response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeAvatar() = withContext(Dispatchers.IO) {
|
suspend fun removeAvatar() = withContext(Dispatchers.IO) {
|
||||||
@@ -306,16 +343,13 @@ class DocmostApi(private val baseUrl: String) {
|
|||||||
|
|
||||||
suspend fun getRecentPages(): List<Page> = withContext(Dispatchers.IO) {
|
suspend fun getRecentPages(): List<Page> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/recent", RecentPageRequest())
|
val json = post("/api/pages/recent", RecentPageRequest())
|
||||||
val type = object : TypeToken<DocmostResponse<RecentPagesResponse>>() {}.type
|
parseJsonArray(json) { gson.fromJson(it, Page::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<RecentPagesResponse>>(json, type)
|
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createPage(title: String, spaceId: String): Page = withContext(Dispatchers.IO) {
|
suspend fun createPage(title: String, spaceId: String): Page = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/create", CreatePageRequest(title = title, spaceId = spaceId))
|
val json = post("/api/pages/create", CreatePageRequest(title = title, spaceId = spaceId))
|
||||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Page::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
?: throw IOException("Create page response missing data")
|
||||||
response.data ?: throw IOException("Create page response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deletePage(pageId: String) = withContext(Dispatchers.IO) {
|
suspend fun deletePage(pageId: String) = withContext(Dispatchers.IO) {
|
||||||
@@ -328,16 +362,13 @@ class DocmostApi(private val baseUrl: String) {
|
|||||||
|
|
||||||
suspend fun getShares(): List<Share> = withContext(Dispatchers.IO) {
|
suspend fun getShares(): List<Share> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/shares/", mapOf("limit" to 50))
|
val json = post("/api/shares/", mapOf("limit" to 50))
|
||||||
val type = object : TypeToken<DocmostResponse<SharesData>>() {}.type
|
parseJsonArray(json) { gson.fromJson(it, Share::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<SharesData>>(json, type)
|
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createShare(pageId: String): Share = withContext(Dispatchers.IO) {
|
suspend fun createShare(pageId: String): Share = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/shares/create", CreateShareDto(pageId = pageId))
|
val json = post("/api/shares/create", CreateShareDto(pageId = pageId))
|
||||||
val type = object : TypeToken<DocmostResponse<Share>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Share::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Share>>(json, type)
|
?: throw IOException("Create share response missing data")
|
||||||
response.data ?: throw IOException("Create share response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteShare(shareId: String) = withContext(Dispatchers.IO) {
|
suspend fun deleteShare(shareId: String) = withContext(Dispatchers.IO) {
|
||||||
@@ -346,23 +377,19 @@ class DocmostApi(private val baseUrl: String) {
|
|||||||
|
|
||||||
suspend fun getComments(pageId: String): List<Comment> = withContext(Dispatchers.IO) {
|
suspend fun getComments(pageId: String): List<Comment> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/comments/", mapOf("pageId" to pageId))
|
val json = post("/api/comments/", mapOf("pageId" to pageId))
|
||||||
val type = object : TypeToken<DocmostResponse<CommentsData>>() {}.type
|
parseJsonArray(json) { gson.fromJson(it, Comment::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<CommentsData>>(json, type)
|
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createComment(pageId: String, content: String): Comment = withContext(Dispatchers.IO) {
|
suspend fun createComment(pageId: String, content: String): Comment = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/comments/create", CreateCommentDto(pageId = pageId, content = content))
|
val json = post("/api/comments/create", CreateCommentDto(pageId = pageId, content = content))
|
||||||
val type = object : TypeToken<DocmostResponse<Comment>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Comment::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Comment>>(json, type)
|
?: throw IOException("Create comment response missing data")
|
||||||
response.data ?: throw IOException("Create comment response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateComment(commentId: String, content: String): Comment = withContext(Dispatchers.IO) {
|
suspend fun updateComment(commentId: String, content: String): Comment = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/comments/update", UpdateCommentDto(commentId = commentId, content = content))
|
val json = post("/api/comments/update", UpdateCommentDto(commentId = commentId, content = content))
|
||||||
val type = object : TypeToken<DocmostResponse<Comment>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Comment::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Comment>>(json, type)
|
?: throw IOException("Update comment response missing data")
|
||||||
response.data ?: throw IOException("Update comment response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteComment(commentId: String) = withContext(Dispatchers.IO) {
|
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) {
|
suspend fun duplicatePage(pageId: String, targetSpaceId: String? = null): Page = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/duplicate", DuplicatePageRequest(pageId = pageId, spaceId = targetSpaceId))
|
val json = post("/api/pages/duplicate", DuplicatePageRequest(pageId = pageId, spaceId = targetSpaceId))
|
||||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, Page::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
?: throw IOException("Duplicate response missing data")
|
||||||
response.data ?: throw IOException("Duplicate response missing data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun movePageToSpace(pageId: String, spaceId: String) = withContext(Dispatchers.IO) {
|
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<PageHistory> = withContext(Dispatchers.IO) {
|
suspend fun getPageHistory(pageId: String): List<PageHistory> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/history", PageHistoryRequest(pageId = pageId))
|
val json = post("/api/pages/history", PageHistoryRequest(pageId = pageId))
|
||||||
val type = object : TypeToken<DocmostResponse<PageHistoryResponse>>() {}.type
|
parseJsonArray(json) { gson.fromJson(it, PageHistory::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<PageHistoryResponse>>(json, type)
|
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPageHistoryInfo(pageId: String, historyId: String): PageHistory = withContext(Dispatchers.IO) {
|
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 json = post("/api/pages/history/info", mapOf("pageId" to pageId, "historyId" to historyId))
|
||||||
val type = object : TypeToken<DocmostResponse<PageHistory>>() {}.type
|
parseJsonObject(json) { gson.fromJson(it, PageHistory::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<PageHistory>>(json, type)
|
?: throw IOException("Page history not found in response")
|
||||||
response.data ?: throw IOException("Page history not found in response")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPageContent(pageId: String): PageContent = withContext(Dispatchers.IO) {
|
suspend fun getPageContent(pageId: String): PageContent = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/info", PageInfoRequest(pageId = pageId))
|
val json = post("/api/pages/info", PageInfoRequest(pageId = pageId))
|
||||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
Log.d("DocmostAPI", "PageContent response: $json")
|
||||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
val element = com.google.gson.JsonParser.parseString(json)
|
||||||
val page = response.data ?: throw IOException("Page not found in response")
|
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 ?: "")
|
PageContent(source = page.source ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDeletedPages(spaceId: String): List<DeletedPage> = withContext(Dispatchers.IO) {
|
suspend fun getDeletedPages(spaceId: String): List<DeletedPage> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/pages/trash", DeletedPagesRequest(spaceId = spaceId))
|
val json = post("/api/pages/trash", DeletedPagesRequest(spaceId = spaceId))
|
||||||
val type = object : TypeToken<DocmostResponse<DeletedPagesResponse>>() {}.type
|
parseJsonArray(json) { gson.fromJson(it, DeletedPage::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<DeletedPagesResponse>>(json, type)
|
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun restorePage(pageId: String, spaceId: String? = null) = withContext(Dispatchers.IO) {
|
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<Attachment> = withContext(Dispatchers.IO) {
|
suspend fun getPageAttachments(pageId: String): List<Attachment> = withContext(Dispatchers.IO) {
|
||||||
val json = post("/api/attachments/page", mapOf("pageId" to pageId))
|
val json = post("/api/attachments/page", mapOf("pageId" to pageId))
|
||||||
val type = object : TypeToken<DocmostResponse<AttachmentsResponse>>() {}.type
|
parseJsonArray(json) { gson.fromJson(it, Attachment::class.java) }
|
||||||
val response = gson.fromJson<DocmostResponse<AttachmentsResponse>>(json, type)
|
|
||||||
response.data?.items ?: emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameFromUri(context: android.content.Context, uri: android.net.Uri): String? {
|
private fun getFileNameFromUri(context: android.content.Context, uri: android.net.Uri): String? {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import com.docmost.app.api.Page
|
import com.docmost.app.api.Page
|
||||||
import com.docmost.app.BuildConfig
|
import com.docmost.app.BuildConfig
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.JsonParser
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ class PageCacheManager(private val context: Context) {
|
|||||||
fun getCachedPages(): List<CachedPageMeta> {
|
fun getCachedPages(): List<CachedPageMeta> {
|
||||||
return try {
|
return try {
|
||||||
if (!indexFile.exists()) return emptyList()
|
if (!indexFile.exists()) return emptyList()
|
||||||
val type = object : TypeToken<List<CachedPageMeta>>() {}.type
|
val array = JsonParser.parseString(indexFile.readText()).asJsonArray
|
||||||
gson.fromJson(indexFile.readText(), type) ?: emptyList()
|
array.map { gson.fromJson(it, CachedPageMeta::class.java) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
@@ -153,8 +153,8 @@ class PageCacheManager(private val context: Context) {
|
|||||||
return try {
|
return try {
|
||||||
val file = File(spacePagesDir, "$spaceId.json")
|
val file = File(spacePagesDir, "$spaceId.json")
|
||||||
if (!file.exists()) return null
|
if (!file.exists()) return null
|
||||||
val type = object : TypeToken<List<Page>>() {}.type
|
val array = JsonParser.parseString(file.readText()).asJsonArray
|
||||||
gson.fromJson(file.readText(), type)
|
array.map { gson.fromJson(it, Page::class.java) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -175,11 +175,27 @@ class PageCacheManager(private val context: Context) {
|
|||||||
|
|
||||||
fun getCachedPageCount(): Int = getIndex().size
|
fun getCachedPageCount(): Int = getIndex().size
|
||||||
|
|
||||||
|
fun clearAllCachedPages(): Int {
|
||||||
|
var count = 0
|
||||||
|
try {
|
||||||
|
val index = getIndex()
|
||||||
|
count = index.size
|
||||||
|
cacheDir.listFiles()?.forEach { file ->
|
||||||
|
if (file.isFile && file.name != "index.json") {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spacePagesDir.listFiles()?.forEach { it.delete() }
|
||||||
|
indexFile.writeText("[]")
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
private fun getIndex(): List<CachedPageMeta> {
|
private fun getIndex(): List<CachedPageMeta> {
|
||||||
return try {
|
return try {
|
||||||
if (!indexFile.exists()) return emptyList()
|
if (!indexFile.exists()) return emptyList()
|
||||||
val type = object : TypeToken<List<CachedPageMeta>>() {}.type
|
val array = JsonParser.parseString(indexFile.readText()).asJsonArray
|
||||||
gson.fromJson(indexFile.readText(), type) ?: emptyList()
|
array.map { gson.fromJson(it, CachedPageMeta::class.java) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.SharedPreferences
|
|||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKeys
|
import androidx.security.crypto.MasterKeys
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
|
||||||
|
|
||||||
data class Credentials(
|
data class Credentials(
|
||||||
@@ -110,8 +111,8 @@ class SessionManager(private val context: Context) {
|
|||||||
null
|
null
|
||||||
} ?: return emptyList()
|
} ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val type = com.google.gson.reflect.TypeToken.getParameterized(List::class.java, Credentials::class.java).type
|
val array = JsonParser.parseString(json).asJsonArray
|
||||||
gson.fromJson(json, type)
|
array.map { gson.fromJson(it, Credentials::class.java) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import com.docmost.app.api.Space
|
import com.docmost.app.api.Space
|
||||||
import com.docmost.app.BuildConfig
|
import com.docmost.app.BuildConfig
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.JsonParser
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@@ -119,8 +119,8 @@ class SpaceCacheManager(private val context: Context) {
|
|||||||
fun getIndex(): List<CachedSpaceMeta> {
|
fun getIndex(): List<CachedSpaceMeta> {
|
||||||
return try {
|
return try {
|
||||||
if (!indexFile.exists()) return emptyList()
|
if (!indexFile.exists()) return emptyList()
|
||||||
val type = object : TypeToken<List<CachedSpaceMeta>>() {}.type
|
val array = JsonParser.parseString(indexFile.readText()).asJsonArray
|
||||||
gson.fromJson(indexFile.readText(), type) ?: emptyList()
|
array.map { gson.fromJson(it, CachedSpaceMeta::class.java) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.room.PrimaryKey
|
|||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
|
|
||||||
enum class OperationType {
|
enum class OperationType {
|
||||||
CREATE_SPACE,
|
CREATE_SPACE,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import com.docmost.app.utils.NetworkConnectivityObserver
|
|||||||
import com.docmost.app.utils.NetworkStatus
|
import com.docmost.app.utils.NetworkStatus
|
||||||
import com.docmost.app.databinding.ActivityPageEditorBinding
|
import com.docmost.app.databinding.ActivityPageEditorBinding
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -284,6 +283,7 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var isEditorRendered = false
|
private var isEditorRendered = false
|
||||||
|
private var isContentLoading = false
|
||||||
private var pendingContent: String? = null
|
private var pendingContent: String? = null
|
||||||
private var isTitleVisible = true
|
private var isTitleVisible = true
|
||||||
private var isAnimating = false
|
private var isAnimating = false
|
||||||
@@ -628,6 +628,7 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun onContentChanged(markdown: String) {
|
fun onContentChanged(markdown: String) {
|
||||||
|
if (isContentLoading) return
|
||||||
currentPageContent = markdown
|
currentPageContent = markdown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,12 +642,15 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
loadEditorContentInternal(content)
|
loadEditorContentInternal(content)
|
||||||
pendingContent = null
|
pendingContent = null
|
||||||
}
|
}
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
isContentLoading = false
|
||||||
|
}, 600)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun onAutoSave(markdown: String) {
|
fun onAutoSave(markdown: String) {
|
||||||
if (!canEdit) return
|
if (!canEdit || isEditorReadOnly) return
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val title = binding.etTitle.text.toString().trim()
|
val title = binding.etTitle.text.toString().trim()
|
||||||
@@ -699,14 +703,14 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
cachedApi.updatePage(
|
cachedApi.updatePage(
|
||||||
currentPageId, markdown,
|
currentPageId, markdown,
|
||||||
if (hasTitleChanges) title else null,
|
title,
|
||||||
if (hasEmojiChanges) currentEmoji else null
|
if (hasEmojiChanges) currentEmoji else null
|
||||||
)
|
)
|
||||||
|
|
||||||
val cachedPage = pageCacheManager.getCachedPage(currentPageId)
|
val cachedPage = pageCacheManager.getCachedPage(currentPageId)
|
||||||
if (cachedPage != null) {
|
if (cachedPage != null) {
|
||||||
pageCacheManager.cachePage(cachedPage.copy(
|
pageCacheManager.cachePage(cachedPage.copy(
|
||||||
title = if (hasTitleChanges) title else cachedPage.title,
|
title = title,
|
||||||
icon = if (hasEmojiChanges) currentEmoji else cachedPage.icon,
|
icon = if (hasEmojiChanges) currentEmoji else cachedPage.icon,
|
||||||
content = markdown
|
content = markdown
|
||||||
))
|
))
|
||||||
@@ -741,7 +745,7 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
cachedApi.updatePage(
|
cachedApi.updatePage(
|
||||||
currentPageId, null,
|
currentPageId, null,
|
||||||
if (hasTitleChanges) title else null,
|
title,
|
||||||
if (hasEmojiChanges) currentEmoji else null
|
if (hasEmojiChanges) currentEmoji else null
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -769,7 +773,7 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
cachedApi.updatePage(
|
cachedApi.updatePage(
|
||||||
pageId!!,
|
pageId!!,
|
||||||
if (hasBodyChanges) markdown else null,
|
if (hasBodyChanges) markdown else null,
|
||||||
if (hasTitleChanges) title else null,
|
title,
|
||||||
if (pageEmoji != originalEmoji) pageEmoji else null
|
if (pageEmoji != originalEmoji) pageEmoji else null
|
||||||
)
|
)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -1005,6 +1009,7 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun loadEditorContent(markdown: String) {
|
private fun loadEditorContent(markdown: String) {
|
||||||
if (BuildConfig.DEBUG) android.util.Log.d("PageEditor", "loadEditorContent called with ${markdown.length} chars")
|
if (BuildConfig.DEBUG) android.util.Log.d("PageEditor", "loadEditorContent called with ${markdown.length} chars")
|
||||||
|
isContentLoading = true
|
||||||
if (isEditorRendered) {
|
if (isEditorRendered) {
|
||||||
loadEditorContentInternal(markdown)
|
loadEditorContentInternal(markdown)
|
||||||
} else {
|
} else {
|
||||||
@@ -1878,8 +1883,8 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun tiptapToMarkdown(tiptapJson: String): String {
|
private fun tiptapToMarkdown(tiptapJson: String): String {
|
||||||
return try {
|
return try {
|
||||||
val mapType = object : TypeToken<Map<String, Any?>>() {}.type
|
val type = object : com.google.gson.reflect.TypeToken<Map<String, Any?>>() {}.type
|
||||||
val doc = gson.fromJson<Map<String, Any?>>(tiptapJson, mapType)
|
val doc: Map<String, Any?> = gson.fromJson(tiptapJson, type)
|
||||||
extractText(doc)
|
extractText(doc)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
tiptapJson
|
tiptapJson
|
||||||
@@ -2090,11 +2095,13 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
canEdit = page.permissions?.canEdit ?: true
|
canEdit = page.permissions?.canEdit ?: true
|
||||||
currentPage = page
|
currentPage = page
|
||||||
|
|
||||||
|
android.util.Log.d("PageEditor", "renderPageContent: page.content type=${page.content?.let { it::class.java.simpleName } ?: "null"}")
|
||||||
|
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
loadReadOnlyPreview(page)
|
loadReadOnlyPreview(page)
|
||||||
} else {
|
} else {
|
||||||
val markdown = contentToMarkdown(page.content)
|
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
|
currentPageContent = markdown
|
||||||
originalContent = markdown
|
originalContent = markdown
|
||||||
binding.etTitle.setText(page.title)
|
binding.etTitle.setText(page.title)
|
||||||
@@ -2484,15 +2491,68 @@ class PageEditorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun contentToMarkdown(content: Any?): String {
|
private fun contentToMarkdown(content: Any?): String {
|
||||||
if (content == null) return ""
|
if (content == null) return ""
|
||||||
if (content is String) return content
|
if (content is String) {
|
||||||
|
return if (content.trimStart().startsWith("<")) {
|
||||||
|
htmlToMarkdown(content)
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (content is Map<*, *>) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return try {
|
||||||
|
extractText(content as Map<String, Any?>)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("PageEditor", "contentToMarkdown map error: ${e.message}")
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
return try {
|
return try {
|
||||||
val json = gson.toJson(content)
|
val json = gson.toJson(content)
|
||||||
tiptapToMarkdown(json)
|
if (json.isNotEmpty() && json != "null") {
|
||||||
|
tiptapToMarkdown(json)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
content.toString()
|
android.util.Log.e("PageEditor", "contentToMarkdown error: ${e.message}")
|
||||||
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun htmlToMarkdown(html: String): String {
|
||||||
|
return html
|
||||||
|
.replace(Regex("<h1[^>]*>(.*?)</h1>", RegexOption.DOT_MATCHES_ALL)) { "# ${it.groupValues[1].trim()}\n\n" }
|
||||||
|
.replace(Regex("<h2[^>]*>(.*?)</h2>", RegexOption.DOT_MATCHES_ALL)) { "## ${it.groupValues[1].trim()}\n\n" }
|
||||||
|
.replace(Regex("<h3[^>]*>(.*?)</h3>", RegexOption.DOT_MATCHES_ALL)) { "### ${it.groupValues[1].trim()}\n\n" }
|
||||||
|
.replace(Regex("<h4[^>]*>(.*?)</h4>", RegexOption.DOT_MATCHES_ALL)) { "#### ${it.groupValues[1].trim()}\n\n" }
|
||||||
|
.replace(Regex("<h5[^>]*>(.*?)</h5>", RegexOption.DOT_MATCHES_ALL)) { "##### ${it.groupValues[1].trim()}\n\n" }
|
||||||
|
.replace(Regex("<h6[^>]*>(.*?)</h6>", RegexOption.DOT_MATCHES_ALL)) { "###### ${it.groupValues[1].trim()}\n\n" }
|
||||||
|
.replace(Regex("<strong[^>]*>(.*?)</strong>", RegexOption.DOT_MATCHES_ALL)) { "**${it.groupValues[1]}**" }
|
||||||
|
.replace(Regex("<b[^>]*>(.*?)</b>", RegexOption.DOT_MATCHES_ALL)) { "**${it.groupValues[1]}**" }
|
||||||
|
.replace(Regex("<em[^>]*>(.*?)</em>", RegexOption.DOT_MATCHES_ALL)) { "*${it.groupValues[1]}*" }
|
||||||
|
.replace(Regex("<i[^>]*>(.*?)</i>", RegexOption.DOT_MATCHES_ALL)) { "*${it.groupValues[1]}*" }
|
||||||
|
.replace(Regex("<del[^>]*>(.*?)</del>", RegexOption.DOT_MATCHES_ALL)) { "~~${it.groupValues[1]}~~" }
|
||||||
|
.replace(Regex("<code[^>]*>(.*?)</code>", RegexOption.DOT_MATCHES_ALL)) { "`${it.groupValues[1]}`" }
|
||||||
|
.replace(Regex("<a[^>]*href=\"([^\"]+)\"[^>]*>(.*?)</a>", RegexOption.DOT_MATCHES_ALL)) { "[${it.groupValues[2]}](${it.groupValues[1]})" }
|
||||||
|
.replace(Regex("<img[^>]*src=\"([^\"]+)\"[^>]*alt=\"([^\"]*)\"[^>]*/?>", RegexOption.DOT_MATCHES_ALL)) { "![${it.groupValues[2]}](${it.groupValues[1]})" }
|
||||||
|
.replace(Regex("<img[^>]*alt=\"([^\"]*)\"[^>]*src=\"([^\"]+)\"[^>]*/?>", RegexOption.DOT_MATCHES_ALL)) { "![${it.groupValues[1]}](${it.groupValues[2]})" }
|
||||||
|
.replace(Regex("<li[^>]*>(.*?)</li>", RegexOption.DOT_MATCHES_ALL)) { "- ${it.groupValues[1].replace(Regex("<[^>]*>"), "").trim()}\n" }
|
||||||
|
.replace(Regex("<blockquote[^>]*>(.*?)</blockquote>", RegexOption.DOT_MATCHES_ALL)) { "> ${it.groupValues[1].replace(Regex("<[^>]*>"), "").trim()}\n\n" }
|
||||||
|
.replace(Regex("<pre[^>]*><code[^>]*>(.*?)</code></pre>", RegexOption.DOT_MATCHES_ALL)) { "```\n${it.groupValues[1].replace(Regex("<[^>]*>"), "")}\n```\n\n" }
|
||||||
|
.replace(Regex("<p[^>]*>(.*?)</p>", RegexOption.DOT_MATCHES_ALL)) { "${it.groupValues[1].replace(Regex("<[^>]*>"), "").trim()}\n\n" }
|
||||||
|
.replace(Regex("<br[^>]*/?>", RegexOption.DOT_MATCHES_ALL)) { "\n" }
|
||||||
|
.replace(Regex("<hr[^>]*/?>", 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 {
|
private fun markdownToTipTap(markdown: String): String {
|
||||||
// This function converts Markdown to TipTap JSON format that Docmost expects
|
// This function converts Markdown to TipTap JSON format that Docmost expects
|
||||||
// It handles attachments specially to preserve the attachment type
|
// It handles attachments specially to preserve the attachment type
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import com.docmost.app.utils.NetworkConnectivityObserver
|
|||||||
import com.docmost.app.utils.NetworkStatus
|
import com.docmost.app.utils.NetworkStatus
|
||||||
import com.docmost.app.databinding.ActivitySpacesBinding
|
import com.docmost.app.databinding.ActivitySpacesBinding
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
@@ -286,6 +285,14 @@ class SpacesActivity : AppCompatActivity() {
|
|||||||
showCachedPagesDialog()
|
showCachedPagesDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnSyncAll.setOnClickListener {
|
||||||
|
startSyncAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnCleanCache.setOnClickListener {
|
||||||
|
showCleanCacheDialog()
|
||||||
|
}
|
||||||
|
|
||||||
binding.btnDeletedPages.setOnClickListener {
|
binding.btnDeletedPages.setOnClickListener {
|
||||||
showDeletedPagesDialog()
|
showDeletedPagesDialog()
|
||||||
}
|
}
|
||||||
@@ -909,8 +916,8 @@ class SpacesActivity : AppCompatActivity() {
|
|||||||
private fun loadRecentCache(): List<Page>? {
|
private fun loadRecentCache(): List<Page>? {
|
||||||
try {
|
try {
|
||||||
val json = prefs.getString("recent_cache", null) ?: return null
|
val json = prefs.getString("recent_cache", null) ?: return null
|
||||||
val type = object : TypeToken<List<Page>>() {}.type
|
val array = com.google.gson.JsonParser.parseString(json).asJsonArray
|
||||||
return gson.fromJson(json, type)
|
return array.map { gson.fromJson(it, Page::class.java) }
|
||||||
} catch (_: Exception) { return null }
|
} catch (_: Exception) { return null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1652,6 +1659,79 @@ class SpacesActivity : AppCompatActivity() {
|
|||||||
Toast.makeText(this, R.string.link_copied, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.link_copied, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var syncDialog: android.app.AlertDialog? = null
|
||||||
|
|
||||||
|
private fun startSyncAll() {
|
||||||
|
if (cachedApi.isOffline) {
|
||||||
|
Toast.makeText(this, R.string.offline_not_available, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (cachedApi.isSyncing) {
|
||||||
|
Toast.makeText(this, R.string.syncing, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressDialog = android.app.AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.syncing)
|
||||||
|
.setMessage(getString(R.string.sync_progress, 0, 0, 0, 0))
|
||||||
|
.setCancelable(true)
|
||||||
|
.setOnCancelListener {
|
||||||
|
cachedApi.stopSync()
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
progressDialog.show()
|
||||||
|
syncDialog = progressDialog
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val (synced, errors) = cachedApi.syncAllPages { progress ->
|
||||||
|
runOnUiThread {
|
||||||
|
if (progressDialog.isShowing) {
|
||||||
|
progressDialog.setMessage(getString(R.string.sync_progress,
|
||||||
|
progress.spaceIndex, progress.totalSpaces,
|
||||||
|
progress.pageCount, progress.totalPages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (progressDialog.isShowing) progressDialog.dismiss()
|
||||||
|
Toast.makeText(this@SpacesActivity,
|
||||||
|
getString(R.string.sync_complete, synced, 0, errors),
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
loadSpaces()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (progressDialog.isShowing) progressDialog.dismiss()
|
||||||
|
if (e.message?.contains("cancelled", true) == true || e is kotlinx.coroutines.CancellationException) {
|
||||||
|
Toast.makeText(this@SpacesActivity, R.string.sync_cancelled, Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@SpacesActivity, e.message ?: "Error", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCleanCacheDialog() {
|
||||||
|
val count = cachedApi.getCachedPageCount()
|
||||||
|
if (count == 0) {
|
||||||
|
Toast.makeText(this, R.string.no_cached_pages, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
com.google.android.material.dialog.MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.clean_cache)
|
||||||
|
.setMessage(getString(R.string.clean_confirm) + "\n\n" + getString(R.string.cached_count, count))
|
||||||
|
.setPositiveButton(R.string.clean_cache) { _, _ ->
|
||||||
|
val removed = cachedApi.clearAllCachedPages()
|
||||||
|
Toast.makeText(this, getString(R.string.cache_cleaned, removed), Toast.LENGTH_SHORT).show()
|
||||||
|
loadSpaces()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showCachedPagesDialog() {
|
private fun showCachedPagesDialog() {
|
||||||
val cachedPages = pageCacheManager.getCachedPages()
|
val cachedPages = pageCacheManager.getCachedPages()
|
||||||
if (cachedPages.isEmpty()) {
|
if (cachedPages.isEmpty()) {
|
||||||
|
|||||||
@@ -109,13 +109,26 @@ class VersionViewerActivity : AppCompatActivity() {
|
|||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val history = api.getPageHistoryInfo(pageId, historyId)
|
val history = api.getPageHistoryInfo(pageId, historyId)
|
||||||
val htmlContent = if (history.content != null) {
|
val htmlContent: String
|
||||||
val gson = Gson()
|
if (history.content != null) {
|
||||||
val json = gson.toJson(history.content)
|
val content = history.content
|
||||||
originalTipTapJson = json
|
if (content is String && content.trimStart().startsWith("<")) {
|
||||||
ContentConverter.tipTapToHtml(json)
|
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 {
|
} else {
|
||||||
"<p>Sin contenido</p>"
|
htmlContent = "<p>Sin contenido</p>"
|
||||||
|
originalTipTapJson = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ object ContentConverter {
|
|||||||
|
|
||||||
fun tipTapJsonToHtml(tipTapJson: String): String {
|
fun tipTapJsonToHtml(tipTapJson: String): String {
|
||||||
return try {
|
return try {
|
||||||
val mapType = object : TypeToken<Map<String, Any?>>() {}.type
|
val type = object : TypeToken<Map<String, Any?>>() {}.type
|
||||||
val doc = gson.fromJson<Map<String, Any?>>(tipTapJson, mapType)
|
val doc: Map<String, Any?> = gson.fromJson(tipTapJson, type)
|
||||||
nodeToHtml(doc)
|
nodeToHtml(doc)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
tipTapJson
|
tipTapJson
|
||||||
@@ -18,14 +18,34 @@ object ContentConverter {
|
|||||||
|
|
||||||
fun tipTapToHtml(tipTapJson: String): String {
|
fun tipTapToHtml(tipTapJson: String): String {
|
||||||
return try {
|
return try {
|
||||||
val mapType = object : TypeToken<Map<String, Any?>>() {}.type
|
val type = object : TypeToken<Map<String, Any?>>() {}.type
|
||||||
val doc = gson.fromJson<Map<String, Any?>>(tipTapJson, mapType)
|
val doc: Map<String, Any?> = gson.fromJson(tipTapJson, type)
|
||||||
nodeToHtml(doc)
|
nodeToHtml(doc)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
tipTapJson
|
tipTapJson
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun contentToHtml(content: Any?): String {
|
||||||
|
if (content == null) return "<p>Sin contenido</p>"
|
||||||
|
if (content is String) {
|
||||||
|
return if (content.trimStart().startsWith("<")) {
|
||||||
|
content
|
||||||
|
} else {
|
||||||
|
tipTapToHtml(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (content is Map<*, *>) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return try {
|
||||||
|
nodeToHtml(content as Map<String, Any?>)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"<p>Error converting content</p>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "<p>Sin contenido</p>"
|
||||||
|
}
|
||||||
|
|
||||||
private fun nodeToHtml(node: Map<String, Any?>): String {
|
private fun nodeToHtml(node: Map<String, Any?>): String {
|
||||||
val type = node["type"] as? String
|
val type = node["type"] as? String
|
||||||
|
|
||||||
@@ -74,7 +94,13 @@ object ContentConverter {
|
|||||||
sb.append("</p>")
|
sb.append("</p>")
|
||||||
}
|
}
|
||||||
"heading" -> {
|
"heading" -> {
|
||||||
val level = (node["attrs"] as? Map<*, *>)?.get("level") as? Int ?: 1
|
val levelRaw = (node["attrs"] as? Map<*, *>)?.get("level")
|
||||||
|
val level = when (levelRaw) {
|
||||||
|
is Int -> levelRaw
|
||||||
|
is Double -> levelRaw.toInt()
|
||||||
|
is Number -> levelRaw.toInt()
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
sb.append("<h$level>")
|
sb.append("<h$level>")
|
||||||
content?.forEach { child ->
|
content?.forEach { child ->
|
||||||
if (child is Map<*, *>) {
|
if (child is Map<*, *>) {
|
||||||
|
|||||||
@@ -471,13 +471,31 @@
|
|||||||
android:text="@string/force_offline"
|
android:text="@string/force_offline"
|
||||||
android:layout_marginBottom="12dp" />
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnSyncAll"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:text="@string/sync_all"
|
||||||
|
android:icon="@android:drawable/ic_menu_save"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnCachedPages"
|
android:id="@+id/btnCachedPages"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:text="@string/cached_pages"
|
android:text="@string/cached_pages"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
style="@style/Widget.Material3.Button.TonalButton" />
|
style="@style/Widget.Material3.Button.TonalButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnCleanCache"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:text="@string/clean_cache"
|
||||||
|
android:icon="@drawable/ic_delete"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|||||||
@@ -117,4 +117,15 @@
|
|||||||
<string name="deleted_pages">Páginas eliminadas</string>
|
<string name="deleted_pages">Páginas eliminadas</string>
|
||||||
<string name="deleted_pages_description">Ver y restaurar páginas eliminadas recientemente</string>
|
<string name="deleted_pages_description">Ver y restaurar páginas eliminadas recientemente</string>
|
||||||
<string name="deleted_pages_button">🗑️ Páginas eliminadas</string>
|
<string name="deleted_pages_button">🗑️ Páginas eliminadas</string>
|
||||||
|
<string name="sync_all">Sincronizar todo</string>
|
||||||
|
<string name="sync_all_description">Descargar todas las páginas para uso sin conexión</string>
|
||||||
|
<string name="clean_cache">Limpiar caché</string>
|
||||||
|
<string name="clean_cache_description">Eliminar todas las páginas descargadas</string>
|
||||||
|
<string name="syncing">Sincronizando…</string>
|
||||||
|
<string name="sync_progress">Espacio %1$d/%2$d · Página %3$d/%4$d</string>
|
||||||
|
<string name="sync_complete">Sincronización completa: %1$d descargadas, %2$d actualizadas, %3$d errores</string>
|
||||||
|
<string name="sync_cancelled">Sincronización cancelada</string>
|
||||||
|
<string name="clean_confirm">¿Eliminar todas las páginas descargadas? Las borradores locales se conservarán.</string>
|
||||||
|
<string name="cache_cleaned">Caché limpiada: %d páginas eliminadas</string>
|
||||||
|
<string name="cached_count">Páginas en caché: %d</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -14,3 +14,12 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "Docmost"
|
rootProject.name = "Docmost"
|
||||||
include(":app")
|
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}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user