Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9f0a80dd4 | |||
| b640f1f063 | |||
| c1eec3cf18 | |||
| f265498ed3 |
@@ -20,8 +20,8 @@ android {
|
||||
applicationId = "com.docmost.app"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 7
|
||||
versionName = "1.0.6"
|
||||
versionCode = 11
|
||||
versionName = "1.0.10"
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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 {
|
||||
@@ -2194,6 +2200,10 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
a {
|
||||
color: #64b5f6 !important;
|
||||
}
|
||||
ul[data-type="taskList"] li > label > input[type="checkbox"] {
|
||||
border-color: #666 !important;
|
||||
accent-color: #369FFF;
|
||||
}
|
||||
""" else ""
|
||||
|
||||
return """
|
||||
@@ -2229,6 +2239,12 @@ class PageEditorActivity : AppCompatActivity() {
|
||||
th, td { border: 1px solid #ddd; padding: 8px; white-space: nowrap; }
|
||||
th { background: #f4f4f4; }
|
||||
a { color: #0066cc; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
ul[data-type="taskList"] { list-style: none; padding-left: 0; margin: 8px 0; }
|
||||
ul[data-type="taskList"] li { display: flex; align-items: flex-start; margin: 4px 0; }
|
||||
ul[data-type="taskList"] li > label { flex-shrink: 0; margin-right: 8px; display: flex; align-items: center; padding-top: 2px; }
|
||||
ul[data-type="taskList"] li > label > input[type="checkbox"] { width: 16px; height: 16px; cursor: default; }
|
||||
ul[data-type="taskList"] li > div { flex: 1; }
|
||||
ul[data-type="taskList"] li > div > p { margin: 0; }
|
||||
$darkModeCss
|
||||
</style>
|
||||
<script>
|
||||
|
||||
@@ -1739,39 +1739,83 @@ class SpacesActivity : AppCompatActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
val view = layoutInflater.inflate(R.layout.dialog_search, null)
|
||||
val view = layoutInflater.inflate(R.layout.dialog_cached_pages, null)
|
||||
val tvTitle = view.findViewById<TextView>(R.id.tvDialogTitle)
|
||||
val tvSubtitle = view.findViewById<TextView>(R.id.tvDialogSubtitle)
|
||||
val etSearch = view.findViewById<android.widget.EditText>(R.id.etSearch)
|
||||
val rvResults = view.findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.rvSearchResults)
|
||||
val tvEmpty = view.findViewById<TextView>(R.id.tvSearchEmpty)
|
||||
etSearch.visibility = View.GONE
|
||||
tvEmpty.visibility = View.GONE
|
||||
val rvResults = view.findViewById<RecyclerView>(R.id.rvCachedPages)
|
||||
val tvEmpty = view.findViewById<TextView>(R.id.tvEmpty)
|
||||
|
||||
val allPages = cachedPages.sortedByDescending { it.updatedAt }.toMutableList()
|
||||
val filteredPages = mutableListOf<com.docmost.app.data.CachedPageMeta>()
|
||||
|
||||
tvTitle.text = getString(R.string.cached_pages)
|
||||
tvSubtitle.text = getString(R.string.cached_count, allPages.size)
|
||||
tvEmpty.text = getString(R.string.no_cached_pages_found)
|
||||
|
||||
rvResults.layoutManager = LinearLayoutManager(this)
|
||||
val results = cachedPages.toMutableList()
|
||||
|
||||
val adapter = object : androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder>() {
|
||||
override fun getItemCount() = results.size
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): androidx.recyclerview.widget.RecyclerView.ViewHolder {
|
||||
val v = layoutInflater.inflate(android.R.layout.simple_list_item_2, parent, false)
|
||||
return object : androidx.recyclerview.widget.RecyclerView.ViewHolder(v) {}
|
||||
val dateFormat = java.text.SimpleDateFormat("dd MMM yyyy", java.util.Locale.getDefault())
|
||||
|
||||
fun formatDate(millis: Long): String {
|
||||
return try {
|
||||
dateFormat.format(java.util.Date(millis))
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int) {
|
||||
val r = results[position]
|
||||
val text1 = holder.itemView.findViewById<android.widget.TextView>(android.R.id.text1)
|
||||
val text2 = holder.itemView.findViewById<android.widget.TextView>(android.R.id.text2)
|
||||
text1.text = if (r.icon.isNullOrBlank()) r.title else "${r.icon} ${r.title}"
|
||||
text2.text = r.spaceId ?: getString(R.string.offline_mode)
|
||||
}
|
||||
|
||||
fun updateList(pages: List<com.docmost.app.data.CachedPageMeta>) {
|
||||
filteredPages.clear()
|
||||
filteredPages.addAll(pages)
|
||||
rvResults.adapter?.notifyDataSetChanged()
|
||||
tvEmpty.visibility = if (filteredPages.isEmpty()) View.VISIBLE else View.GONE
|
||||
rvResults.visibility = if (filteredPages.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
val adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun getItemCount() = filteredPages.size
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val v = layoutInflater.inflate(R.layout.item_cached_page, parent, false)
|
||||
return object : RecyclerView.ViewHolder(v) {}
|
||||
}
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val r = filteredPages[position]
|
||||
val tvIcon = holder.itemView.findViewById<TextView>(R.id.tvPageIcon)
|
||||
val tvItemTitle = holder.itemView.findViewById<TextView>(R.id.tvPageTitle)
|
||||
val tvDate = holder.itemView.findViewById<TextView>(R.id.tvPageDate)
|
||||
|
||||
tvIcon.text = if (r.icon.isNullOrBlank()) "📄" else r.icon
|
||||
tvItemTitle.text = r.title
|
||||
tvDate.text = formatDate(r.updatedAt)
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
openPageEditor(Page(id = r.pageId, title = r.title, icon = r.icon, spaceId = r.spaceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
rvResults.adapter = adapter
|
||||
updateList(allPages)
|
||||
|
||||
val maxHeight = (resources.displayMetrics.heightPixels * 0.6).toInt()
|
||||
val maxHeight = (resources.displayMetrics.heightPixels * 0.7).toInt()
|
||||
rvResults.layoutParams.height = maxHeight
|
||||
|
||||
val dialog = BottomSheetDialog(this)
|
||||
etSearch.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val query = s?.toString()?.trim() ?: ""
|
||||
if (query.isEmpty()) {
|
||||
updateList(allPages)
|
||||
return
|
||||
}
|
||||
val lowerQuery = query.lowercase()
|
||||
val filtered = allPages.filter { it.title.lowercase().contains(lowerQuery) }
|
||||
updateList(filtered)
|
||||
}
|
||||
})
|
||||
|
||||
val dialog = com.google.android.material.bottomsheet.BottomSheetDialog(this)
|
||||
dialog.setContentView(view)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
@@ -140,6 +140,29 @@ object ContentConverter {
|
||||
}
|
||||
sb.append("</li>")
|
||||
}
|
||||
"taskList" -> {
|
||||
sb.append("<ul data-type=\"taskList\">")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</ul>")
|
||||
}
|
||||
"taskItem" -> {
|
||||
val checked = (node["attrs"] as? Map<*, *>)?.get("checked") as? Boolean ?: false
|
||||
val checkedAttr = if (checked) " checked" else ""
|
||||
sb.append("<li data-type=\"taskItem\" data-checked=\"$checked\">")
|
||||
sb.append("<label><input type=\"checkbox\"$checkedAttr></label>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</li>")
|
||||
}
|
||||
"codeBlock" -> {
|
||||
sb.append("<pre><code>")
|
||||
content?.forEach { child ->
|
||||
|
||||
57
app/src/main/res/layout/dialog_cached_pages.xml
Normal file
57
app/src/main/res/layout/dialog_cached_pages.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDialogTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDialogSubtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:hint="@string/search_cached_pages"
|
||||
android:inputType="text"
|
||||
android:textSize="15sp"
|
||||
android:background="@drawable/bg_search"
|
||||
android:drawableStart="@drawable/ic_search"
|
||||
android:drawablePadding="10dp"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:maxLines="1"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvCachedPages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmpty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
45
app/src/main/res/layout/item_cached_page.xml
Normal file
45
app/src/main/res/layout/item_cached_page.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageIcon"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:gravity="center"
|
||||
android:textSize="22sp"
|
||||
android:text="📄" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageDate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -128,4 +128,6 @@
|
||||
<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>
|
||||
<string name="search_cached_pages">Buscar páginas guardadas…</string>
|
||||
<string name="no_cached_pages_found">No se encontraron páginas</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user