Merge: notebook zip backup export
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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._-]"), "_")
|
name.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,16 +31,24 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.db.EngPadDatabase
|
||||||
import net.metacircular.engpad.data.model.Notebook
|
import net.metacircular.engpad.data.model.Notebook
|
||||||
import net.metacircular.engpad.data.model.PageSize
|
import net.metacircular.engpad.data.model.PageSize
|
||||||
import net.metacircular.engpad.data.repository.NotebookRepository
|
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.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -62,6 +70,9 @@ fun NotebookListScreen(
|
|||||||
val repository = remember {
|
val repository = remember {
|
||||||
NotebookRepository(database.notebookDao(), database.pageDao())
|
NotebookRepository(database.notebookDao(), database.pageDao())
|
||||||
}
|
}
|
||||||
|
val pageRepository = remember {
|
||||||
|
PageRepository(database.pageDao(), database.strokeDao())
|
||||||
|
}
|
||||||
val viewModel: NotebookListViewModel = viewModel(
|
val viewModel: NotebookListViewModel = viewModel(
|
||||||
factory = NotebookListViewModel.Factory(repository),
|
factory = NotebookListViewModel.Factory(repository),
|
||||||
)
|
)
|
||||||
@@ -69,6 +80,8 @@ fun NotebookListScreen(
|
|||||||
var showCreateDialog by remember { mutableStateOf(false) }
|
var showCreateDialog by remember { mutableStateOf(false) }
|
||||||
var notebookToDelete by remember { mutableStateOf<Notebook?>(null) }
|
var notebookToDelete by remember { mutableStateOf<Notebook?>(null) }
|
||||||
var notebookToRename 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 filterText by remember { mutableStateOf("") }
|
||||||
var sortField by remember { mutableStateOf(SortField.LAST_EDITED) }
|
var sortField by remember { mutableStateOf(SortField.LAST_EDITED) }
|
||||||
@@ -171,6 +184,16 @@ fun NotebookListScreen(
|
|||||||
onClick = { onNotebookClick(notebook.id) },
|
onClick = { onNotebookClick(notebook.id) },
|
||||||
onRename = { notebookToRename = notebook },
|
onRename = { notebookToRename = notebook },
|
||||||
onDelete = { notebookToDelete = 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,
|
onClick: () -> Unit,
|
||||||
onRename: () -> Unit,
|
onRename: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
|
onExportBackup: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) }
|
val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) }
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
@@ -262,6 +286,10 @@ private fun NotebookItem(
|
|||||||
text = { Text("Rename") },
|
text = { Text("Rename") },
|
||||||
onClick = { showMenu = false; onRename() },
|
onClick = { showMenu = false; onRename() },
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Export backup") },
|
||||||
|
onClick = { showMenu = false; onExportBackup() },
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Delete") },
|
text = { Text("Delete") },
|
||||||
onClick = { showMenu = false; onDelete() },
|
onClick = { showMenu = false; onDelete() },
|
||||||
|
|||||||
Reference in New Issue
Block a user