Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f265498ed3 | |||
| 57f8858708 |
@@ -20,8 +20,8 @@ android {
|
||||
applicationId = "com.docmost.app"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 6
|
||||
versionName = "1.0.5"
|
||||
versionCode = 8
|
||||
versionName = "1.0.7"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -58,6 +58,13 @@ android {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -531,12 +531,9 @@
|
||||
} else {
|
||||
console.log('AndroidInterface not available yet');
|
||||
}
|
||||
// Android controla la carga de contenido via setContent()
|
||||
// No cargar pendingAndroidContent automáticamente
|
||||
// Clear pending set flag on next tick after initialization
|
||||
setTimeout(function() {
|
||||
window._pendingSet = false;
|
||||
}, 0);
|
||||
// _pendingSet is cleared by setContent() after 100ms
|
||||
// Do NOT clear it here — doing so prematurely lets onChange
|
||||
// fire during initial content load and sets userEdited=true
|
||||
// Configurar observer para detectar cambios en bloques vacíos
|
||||
setTimeout(function() {
|
||||
setupBackspaceHandler();
|
||||
@@ -617,7 +614,7 @@
|
||||
window.setContentLock = false;
|
||||
setTimeout(function() {
|
||||
window._pendingSet = false;
|
||||
}, 100);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
window.getContent = function() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.docmost.app.api
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.docmost.app.BuildConfig
|
||||
import com.docmost.app.utils.NetworkConnectivityObserver
|
||||
import com.docmost.app.data.PageCacheManager
|
||||
import com.docmost.app.data.SpaceCacheManager
|
||||
@@ -18,10 +19,23 @@ class CachedApi(
|
||||
private val networkObserver: NetworkConnectivityObserver
|
||||
) {
|
||||
var forcedOffline: Boolean = false
|
||||
var isSyncing: Boolean = false
|
||||
private set
|
||||
|
||||
val isOffline: Boolean
|
||||
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 {
|
||||
return try {
|
||||
block()
|
||||
@@ -365,4 +379,96 @@ class CachedApi(
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -175,6 +175,22 @@ class PageCacheManager(private val context: Context) {
|
||||
|
||||
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> {
|
||||
return try {
|
||||
if (!indexFile.exists()) return emptyList()
|
||||
|
||||
@@ -283,6 +283,7 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private var isEditorRendered = false
|
||||
private var isContentLoading = false
|
||||
private var pendingContent: String? = null
|
||||
private var isTitleVisible = true
|
||||
private var isAnimating = false
|
||||
@@ -627,6 +628,7 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
|
||||
@JavascriptInterface
|
||||
fun onContentChanged(markdown: String) {
|
||||
if (isContentLoading) return
|
||||
currentPageContent = markdown
|
||||
}
|
||||
|
||||
@@ -640,12 +642,15 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
loadEditorContentInternal(content)
|
||||
pendingContent = null
|
||||
}
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
isContentLoading = false
|
||||
}, 600)
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun onAutoSave(markdown: String) {
|
||||
if (!canEdit) return
|
||||
if (!canEdit || isEditorReadOnly) return
|
||||
|
||||
runOnUiThread {
|
||||
val title = binding.etTitle.text.toString().trim()
|
||||
@@ -698,14 +703,14 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
try {
|
||||
cachedApi.updatePage(
|
||||
currentPageId, markdown,
|
||||
if (hasTitleChanges) title else null,
|
||||
title,
|
||||
if (hasEmojiChanges) currentEmoji else null
|
||||
)
|
||||
|
||||
val cachedPage = pageCacheManager.getCachedPage(currentPageId)
|
||||
if (cachedPage != null) {
|
||||
pageCacheManager.cachePage(cachedPage.copy(
|
||||
title = if (hasTitleChanges) title else cachedPage.title,
|
||||
title = title,
|
||||
icon = if (hasEmojiChanges) currentEmoji else cachedPage.icon,
|
||||
content = markdown
|
||||
))
|
||||
@@ -740,7 +745,7 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
try {
|
||||
cachedApi.updatePage(
|
||||
currentPageId, null,
|
||||
if (hasTitleChanges) title else null,
|
||||
title,
|
||||
if (hasEmojiChanges) currentEmoji else null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
@@ -768,7 +773,7 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
cachedApi.updatePage(
|
||||
pageId!!,
|
||||
if (hasBodyChanges) markdown else null,
|
||||
if (hasTitleChanges) title else null,
|
||||
title,
|
||||
if (pageEmoji != originalEmoji) pageEmoji else null
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -1004,6 +1009,7 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
|
||||
private fun loadEditorContent(markdown: String) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.d("PageEditor", "loadEditorContent called with ${markdown.length} chars")
|
||||
isContentLoading = true
|
||||
if (isEditorRendered) {
|
||||
loadEditorContentInternal(markdown)
|
||||
} else {
|
||||
|
||||
@@ -285,6 +285,14 @@ class SpacesActivity : AppCompatActivity() {
|
||||
showCachedPagesDialog()
|
||||
}
|
||||
|
||||
binding.btnSyncAll.setOnClickListener {
|
||||
startSyncAll()
|
||||
}
|
||||
|
||||
binding.btnCleanCache.setOnClickListener {
|
||||
showCleanCacheDialog()
|
||||
}
|
||||
|
||||
binding.btnDeletedPages.setOnClickListener {
|
||||
showDeletedPagesDialog()
|
||||
}
|
||||
@@ -1651,6 +1659,79 @@ class SpacesActivity : AppCompatActivity() {
|
||||
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() {
|
||||
val cachedPages = pageCacheManager.getCachedPages()
|
||||
if (cachedPages.isEmpty()) {
|
||||
|
||||
@@ -471,13 +471,31 @@
|
||||
android:text="@string/force_offline"
|
||||
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
|
||||
android:id="@+id/btnCachedPages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/cached_pages"
|
||||
android:layout_marginBottom="8dp"
|
||||
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>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -117,4 +117,15 @@
|
||||
<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_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>
|
||||
|
||||
Reference in New Issue
Block a user