Merge: notebook zip backup export

This commit is contained in:
2026-03-24 20:47:06 -07:00
3 changed files with 100 additions and 1 deletions

View File

@@ -0,0 +1,71 @@
package net.metacircular.engpad.ui.export
import android.content.Context
import net.metacircular.engpad.data.db.toFloatArray
import net.metacircular.engpad.data.repository.NotebookRepository
import net.metacircular.engpad.data.repository.PageRepository
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
object BackupExporter {
suspend fun exportNotebook(
context: Context,
notebookId: Long,
notebookRepository: NotebookRepository,
pageRepository: PageRepository,
): File {
val notebook = notebookRepository.getById(notebookId)
?: throw IllegalArgumentException("Notebook $notebookId not found")
val pages = pageRepository.getPagesList(notebookId)
.sortedBy { it.pageNumber }
val exportDir = File(context.cacheDir, "exports")
exportDir.mkdirs()
val sanitizedTitle = PdfExporter.sanitize(notebook.title)
val file = File(exportDir, "$sanitizedTitle.engpad.zip")
ZipOutputStream(file.outputStream()).use { zip ->
val notebookJson = JSONObject().apply {
put("title", notebook.title)
put("page_size", notebook.pageSize)
put("page_count", pages.size)
}
zip.putNextEntry(ZipEntry("notebook.json"))
zip.write(notebookJson.toString(2).toByteArray())
zip.closeEntry()
for (page in pages) {
val strokes = pageRepository.getStrokes(page.id)
val strokesJson = JSONArray()
for (stroke in strokes) {
val points = stroke.pointData.toFloatArray()
val pointsArray = JSONArray()
for (p in points) {
pointsArray.put(p.toDouble())
}
strokesJson.put(JSONObject().apply {
put("pen_size", stroke.penSize.toDouble())
put("color", stroke.color)
put("style", stroke.style)
put("points", pointsArray)
})
}
val pageJson = JSONObject().apply {
put("page_number", page.pageNumber)
put("strokes", strokesJson)
}
val entryName = "pages/%03d.json".format(page.pageNumber)
zip.putNextEntry(ZipEntry(entryName))
zip.write(pageJson.toString(2).toByteArray())
zip.closeEntry()
}
}
return file
}
}

View File

@@ -124,6 +124,6 @@ object PdfExporter {
}
}
private fun sanitize(name: String): String =
internal fun sanitize(name: String): String =
name.replace(Regex("[^a-zA-Z0-9._-]"), "_")
}

View File

@@ -31,16 +31,24 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.metacircular.engpad.data.db.EngPadDatabase
import net.metacircular.engpad.data.model.Notebook
import net.metacircular.engpad.data.model.PageSize
import net.metacircular.engpad.data.repository.NotebookRepository
import net.metacircular.engpad.data.repository.PageRepository
import net.metacircular.engpad.ui.export.BackupExporter
import net.metacircular.engpad.ui.export.PdfExporter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -62,6 +70,9 @@ fun NotebookListScreen(
val repository = remember {
NotebookRepository(database.notebookDao(), database.pageDao())
}
val pageRepository = remember {
PageRepository(database.pageDao(), database.strokeDao())
}
val viewModel: NotebookListViewModel = viewModel(
factory = NotebookListViewModel.Factory(repository),
)
@@ -69,6 +80,8 @@ fun NotebookListScreen(
var showCreateDialog by remember { mutableStateOf(false) }
var notebookToDelete by remember { mutableStateOf<Notebook?>(null) }
var notebookToRename by remember { mutableStateOf<Notebook?>(null) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
var filterText by remember { mutableStateOf("") }
var sortField by remember { mutableStateOf(SortField.LAST_EDITED) }
@@ -171,6 +184,16 @@ fun NotebookListScreen(
onClick = { onNotebookClick(notebook.id) },
onRename = { notebookToRename = notebook },
onDelete = { notebookToDelete = notebook },
onExportBackup = {
scope.launch {
val file = withContext(Dispatchers.IO) {
BackupExporter.exportNotebook(
context, notebook.id, repository, pageRepository,
)
}
PdfExporter.shareFile(context, file, "application/zip")
}
},
)
}
}
@@ -217,6 +240,7 @@ private fun NotebookItem(
onClick: () -> Unit,
onRename: () -> Unit,
onDelete: () -> Unit,
onExportBackup: () -> Unit,
) {
val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) }
var showMenu by remember { mutableStateOf(false) }
@@ -262,6 +286,10 @@ private fun NotebookItem(
text = { Text("Rename") },
onClick = { showMenu = false; onRename() },
)
DropdownMenuItem(
text = { Text("Export backup") },
onClick = { showMenu = false; onExportBackup() },
)
DropdownMenuItem(
text = { Text("Delete") },
onClick = { showMenu = false; onDelete() },