Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export/Import all notes to/from a single JSON file #161

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
id("kotlin-android")
id("org.jetbrains.kotlin.plugin.compose") version libs.versions.kotlin.get()
id("com.squareup.sqldelight")
kotlin("plugin.serialization") version "2.1.0"
}

repositories {
Expand Down Expand Up @@ -111,6 +112,7 @@ dependencies {
implementation(libs.okio)
implementation(libs.sqldelight)
implementation(libs.systemuicontroller)
implementation(libs.kotlinjson)
debugImplementation(libs.compose.ui.tooling)
coreLibraryDesugaring(libs.android.coreLibraryDesugaring)
}
4 changes: 4 additions & 0 deletions app/src/main/java/com/farmerbb/notepad/ui/components/Menus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ fun NoteListMenu(
onMoreClick: () -> Unit,
onSettingsClick: () -> Unit,
onImportClick: () -> Unit,
onImportAllClick: () -> Unit,
onExportAllClick: () -> Unit,
onAboutClick: () -> Unit
) {
Box {
Expand All @@ -41,6 +43,8 @@ fun NoteListMenu(
) {
MenuItem(R.string.action_settings, onSettingsClick)
MenuItem(R.string.import_notes, onImportClick)
MenuItem(R.string.import_all_notes, onImportAllClick)
MenuItem(R.string.export_all_notes, onExportAllClick)
MenuItem(R.string.dialog_about_title, onAboutClick)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,14 @@ private fun NotepadComposeApp(
onDismiss()
vm.importNotes()
},
onImportAllClick = {
onDismiss()
vm.importAllNotes()
},
onExportAllClick = {
onDismiss()
vm.exportAllNotes()
},
onAboutClick = {
onDismiss()
showAboutDialog = true
Expand Down
81 changes: 81 additions & 0 deletions app/src/main/java/com/farmerbb/notepad/usecase/ArtVandelay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.farmerbb.notepad.model.Note
import com.farmerbb.notepad.model.NoteMetadata
import com.github.k1rakishou.fsaf.FileChooser
import com.github.k1rakishou.fsaf.FileManager
import com.github.k1rakishou.fsaf.callback.FileChooserCallback
import com.github.k1rakishou.fsaf.callback.FileCreateCallback
import com.github.k1rakishou.fsaf.callback.FileMultiSelectChooserCallback
import com.github.k1rakishou.fsaf.callback.directory.DirectoryChooserCallback
Expand All @@ -33,6 +34,7 @@ import java.io.InputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date
import org.koin.dsl.module

interface ArtVandelay {
Expand All @@ -49,6 +51,18 @@ interface ArtVandelay {
onComplete: () -> Unit
)

fun importAllNotes(
saveImportedNotes: (InputStream) -> Unit,
onComplete: () -> Unit
)

fun exportAllNotes(
hydratedNotes: List<Note>,
saveExportedNotes: (OutputStream, List<Note>) -> Unit,
onCancel: () -> Unit,
onComplete: () -> Unit
)

fun exportSingleNote(
metadata: NoteMetadata,
filenameFormat: FilenameFormat,
Expand All @@ -68,6 +82,27 @@ private class ArtVandelayImpl(
importCallback(saveImportedNote, onComplete)
)

override fun importAllNotes(
saveImportedNotes: (InputStream) -> Unit,
onComplete: () -> Unit
) = fileChooser.openChooseFileDialog(
importAllCallback(saveImportedNotes, onComplete)
)

override fun exportAllNotes(
hydratedNotes: List<Note>,
saveExportedNotes: (OutputStream, List<Note>) -> Unit,
onCancel: () -> Unit,
onComplete: () -> Unit,
) = fileChooser.openCreateFileDialog(
fileName = generateExportFilename(),
fileCreateCallback = exportAllCallback(
hydratedNotes,
saveExportedNotes,
onCancel,
onComplete)
)

override fun exportNotes(
hydratedNotes: List<Note>,
filenameFormat: FilenameFormat,
Expand Down Expand Up @@ -119,6 +154,43 @@ private class ArtVandelayImpl(
override fun onCancel(reason: String) = Unit // no-op
}

private fun importAllCallback(
saveImportedNotes: (InputStream) -> Unit,
onComplete: () -> Unit
) = object : FileChooserCallback() {
override fun onResult(uri: Uri) {
with(fileManager) {
fromUri(uri)?.let { file ->
val inputStream = getInputStream(file)
if (inputStream != null) {
saveImportedNotes(inputStream)
}
}

onComplete()
}
}

override fun onCancel(reason: String) = Unit // no-op
}

private fun exportAllCallback(
hydratedNotes: List<Note>,
saveExportedNotes: (OutputStream, List<Note>) -> Unit,
onCancel: () -> Unit,
onComplete: () -> Unit
) = object: FileCreateCallback() {

override fun onResult(uri: Uri) {
with(fileManager) {
fromUri(uri)?.let(::getOutputStream)?.let{ output -> saveExportedNotes(output, hydratedNotes) }
onComplete()
}
}

override fun onCancel(reason: String) = onCancel()
}


private fun exportFolderCallback(
hydratedNotes: List<Note>,
Expand Down Expand Up @@ -166,6 +238,15 @@ private class ArtVandelayImpl(
override fun onCancel(reason: String) = Unit // no-op
}

private fun generateExportFilename(): String {
val title = "notepad_export"
val dateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm", Locale.getDefault())
val timestamp = dateFormat.format(Date())
val filename = "${title}_$timestamp"

return "$filename.json.txt"
}

private fun generateFilename(
metadata: NoteMetadata,
filenameFormat: FilenameFormat
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/com/farmerbb/notepad/utils/Serialization.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.farmerbb.notepad.utils

import com.farmerbb.notepad.model.Note

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import java.text.SimpleDateFormat
import java.util.*


@Serializable
data class SerializableNote(val text: String, val title: String, val date: String)

data class DeserializedNote(val text: String, val title: String, val date: Date)

fun serializeNotes(notes: List<Note>) : String {
val serializableNotes = notes.map { note ->
val date = SimpleDateFormat("yyyy-MM-dd-HH-mm", Locale.getDefault()).format(note.date) ?: ""
SerializableNote(note.text, note.title, date) }
return Json.encodeToString(serializableNotes)
}

fun deserializeNoteJson(text: String): List<DeserializedNote> {
val deserializedNotes =Json.decodeFromString<List<SerializableNote>>(text)
return deserializedNotes.map { note ->
DeserializedNote(note.text, note.title,
SimpleDateFormat("yyyy-MM-dd-HH-mm", Locale.getDefault()).parse(note.date) ?: Date()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import com.farmerbb.notepad.usecase.KeyboardShortcuts
import com.farmerbb.notepad.usecase.SystemTheme
import com.farmerbb.notepad.usecase.Toaster
import com.farmerbb.notepad.utils.checkForUpdates
import com.farmerbb.notepad.utils.deserializeNoteJson
import com.farmerbb.notepad.utils.serializeNotes
import com.farmerbb.notepad.utils.showShareSheet
import de.schnettler.datastore.manager.DataStoreManager
import java.io.InputStream
Expand All @@ -52,6 +54,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.buffer
Expand Down Expand Up @@ -322,6 +325,30 @@ class NotepadViewModel(
}
}

fun importAllNotes() = artVandelay.importAllNotes(::saveImportedNotes) {
viewModelScope.launch {
toaster.toast(R.string.notes_imported_successfully)
}
}

fun exportAllNotes() = viewModelScope.launch(Dispatchers.IO) {
val allNoteMetadata = noteMetadata.first()
val hydratedNotes = repo.getNotes(
allNoteMetadata
)

artVandelay.exportAllNotes(
hydratedNotes,
::saveExportedNotes,
{}
) {
viewModelScope.launch {
toaster.toast(R.string.notes_exported_to)
}
}

}

fun exportNotes(
metadata: List<NoteMetadata>,
filenameFormat: FilenameFormat
Expand Down Expand Up @@ -403,6 +430,29 @@ class NotepadViewModel(
}
}

private fun saveImportedNotes(
input: InputStream,
) = viewModelScope.launch(Dispatchers.IO) {
input.source().buffer().use {
val text = it.readUtf8()
if (text.isNotEmpty()) {
val deserializedNotes = deserializeNoteJson(text)
deserializedNotes.forEach { note ->
repo.saveNote(text = note.text, date = note.date)
}
}
}
}

private fun saveExportedNotes(
output: OutputStream,
notes: List<Note>
) = viewModelScope.launch(Dispatchers.IO) {
output.sink().buffer().use {
it.writeUtf8(serializeNotes(notes))
}
}

private fun saveExportedNote(
output: OutputStream,
text: String
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
<string name="first_run">Welcome to Notepad!\n\nTo create a note, click the New Note button (plus sign).</string>
<string name="first_view">To edit a saved note, double-tap the text or click the Edit button (pencil).</string>
<string name="import_notes">Import notes</string>
<string name="import_all_notes">Import all notes</string>
<string name="export_all_notes">Export all notes</string>
<string name="loading_external_file">Failed loading external content (or content type not supported)</string>
<string name="no_notes_found">No notes found</string>
<string name="note_deleted">Note deleted</string>
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ okio = "3.9.0"
richtext = "0.15.0"
sqldelight = "1.5.5"
versionsPlugin = "0.42.0"
kotlinjson = "1.7.3"

##################################################################################################################################

Expand Down Expand Up @@ -79,6 +80,8 @@ linkifyText = { module = "com.github.firefinchdev:linkify-text", version.ref = "
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
sqldelight = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
kotlinjson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinjson"}


##################################################################################################################################

Expand Down