From 1d00cec0b483b09e649bd0bbe7dabe734987d48c Mon Sep 17 00:00:00 2001 From: Crustack Date: Sun, 12 Apr 2026 16:44:08 +0200 Subject: [PATCH 1/5] Add Quillpad Importer --- README.md | 1 + .../notallyx/data/imports/NotesImporter.kt | 9 + .../data/imports/google/GoogleKeepImporter.kt | 3 +- .../data/imports/google/GoogleKeepNote.kt | 12 +- .../data/imports/quillpad/QuillpadBackup.kt | 60 +++++ .../data/imports/quillpad/QuillpadImporter.kt | 232 ++++++++++++++++++ .../philkes/notallyx/utils/IOExtensions.kt | 16 +- app/src/main/res/drawable/icon_quillpad.xml | 9 + app/src/main/res/values/strings.xml | 3 + app/translations.xlsx | Bin 169513 -> 169996 bytes 10 files changed, 340 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadBackup.kt create mode 100644 app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadImporter.kt create mode 100644 app/src/main/res/drawable/icon_quillpad.xml diff --git a/README.md b/README.md index f9cc235d..ab469fb4 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ * Use **Home Screen Widget** to access important notes fast * **Lock your notes via Biometric/PIN** * Configurable **auto-backups** +* **Import from other apps** such as [Evernote](https://evernote.com/), [Google Keep](https://keep.google.com/), [Quillpad](https://quillpad.github.io/) * Create quick audio notes * Display the notes either in a **List or Grid** * Quickly share notes by text diff --git a/app/src/main/java/com/philkes/notallyx/data/imports/NotesImporter.kt b/app/src/main/java/com/philkes/notallyx/data/imports/NotesImporter.kt index d7a2d3bf..575f3d64 100644 --- a/app/src/main/java/com/philkes/notallyx/data/imports/NotesImporter.kt +++ b/app/src/main/java/com/philkes/notallyx/data/imports/NotesImporter.kt @@ -10,6 +10,7 @@ import com.philkes.notallyx.data.NotallyDatabase import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH import com.philkes.notallyx.data.imports.evernote.EvernoteImporter import com.philkes.notallyx.data.imports.google.GoogleKeepImporter +import com.philkes.notallyx.data.imports.quillpad.QuillpadImporter import com.philkes.notallyx.data.imports.txt.JsonImporter import com.philkes.notallyx.data.imports.txt.PlainTextImporter import com.philkes.notallyx.data.model.Audio @@ -44,6 +45,7 @@ class NotesImporter(private val app: Application, private val database: NotallyD try { when (importSource) { ImportSource.GOOGLE_KEEP -> GoogleKeepImporter() + ImportSource.QUILLPAD -> QuillpadImporter() ImportSource.EVERNOTE -> EvernoteImporter() ImportSource.PLAIN_TEXT -> PlainTextImporter() ImportSource.JSON -> JsonImporter() @@ -194,6 +196,13 @@ enum class ImportSource( "https://help.evernote.com/hc/en-us/articles/209005557-Export-notes-and-notebooks-as-ENEX-or-HTML", R.drawable.icon_evernote, ), + QUILLPAD( + R.string.quillpad, + MIME_TYPE_ZIP, + R.string.quillpad_help, + "https://quillpad.github.io/", + R.drawable.icon_quillpad, + ), PLAIN_TEXT( R.string.plain_text_files, FOLDER_OR_FILE_MIMETYPE, diff --git a/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepImporter.kt b/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepImporter.kt index c05362d6..5b541024 100644 --- a/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepImporter.kt +++ b/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepImporter.kt @@ -25,11 +25,12 @@ import java.io.InputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.json.Json +@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) class GoogleKeepImporter : ExternalImporter { - @OptIn(ExperimentalSerializationApi::class) private val json = Json { ignoreUnknownKeys = true isLenient = true diff --git a/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepNote.kt b/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepNote.kt index 8582e05a..03399e6f 100644 --- a/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepNote.kt +++ b/app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepNote.kt @@ -1,8 +1,10 @@ package com.philkes.notallyx.data.imports.google import com.philkes.notallyx.data.model.BaseNote +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.Serializable +@InternalSerializationApi @Serializable data class GoogleKeepNote( val attachments: List = listOf(), @@ -19,8 +21,12 @@ data class GoogleKeepNote( val listContent: List = listOf(), ) -@Serializable data class GoogleKeepLabel(val name: String) +@InternalSerializationApi @Serializable data class GoogleKeepLabel(val name: String) -@Serializable data class GoogleKeepAttachment(val filePath: String, val mimetype: String) +@InternalSerializationApi +@Serializable +data class GoogleKeepAttachment(val filePath: String, val mimetype: String) -@Serializable data class GoogleKeepListItem(val text: String, val isChecked: Boolean) +@InternalSerializationApi +@Serializable +data class GoogleKeepListItem(val text: String, val isChecked: Boolean) diff --git a/app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadBackup.kt b/app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadBackup.kt new file mode 100644 index 00000000..fbc05fad --- /dev/null +++ b/app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadBackup.kt @@ -0,0 +1,60 @@ +package com.philkes.notallyx.data.imports.quillpad + +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.Serializable + +@InternalSerializationApi +@Serializable +data class QuillpadBackup( + val notes: List = emptyList(), + val notebooks: List = emptyList(), + val tags: List = emptyList(), + val reminders: List = emptyList(), + val joins: List = emptyList(), +) + +@InternalSerializationApi +@Serializable +data class QuillpadNote( + val id: Long, + val title: String? = null, + val content: String? = null, + val isList: Boolean = false, + val taskList: List? = null, + val isPinned: Boolean = false, + val isArchived: Boolean = false, + val isDeleted: Boolean = false, + val creationDate: Long, + val modifiedDate: Long, + val notebookId: Long? = null, + val tags: List? = null, + val attachments: List? = null, + val reminders: List = emptyList(), +) + +@InternalSerializationApi +@Serializable +data class QuillpadTask(val id: Int, val content: String, val isDone: Boolean) + +@InternalSerializationApi @Serializable data class QuillpadNotebook(val id: Long, val name: String) + +@InternalSerializationApi @Serializable data class QuillpadTag(val id: Long, val name: String) + +@InternalSerializationApi +@Serializable +data class QuillpadReminder( + val id: Long, + val noteId: Long, + val date: Long, + val name: String? = null, +) + +@InternalSerializationApi @Serializable data class QuillpadJoin(val tagId: Long, val noteId: Long) + +@InternalSerializationApi +@Serializable +data class QuillpadAttachment( + val type: String? = null, + val description: String? = null, + val fileName: String, +) diff --git a/app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadImporter.kt b/app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadImporter.kt new file mode 100644 index 00000000..38cb0582 --- /dev/null +++ b/app/src/main/java/com/philkes/notallyx/data/imports/quillpad/QuillpadImporter.kt @@ -0,0 +1,232 @@ +package com.philkes.notallyx.data.imports.quillpad + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import com.philkes.notallyx.R +import com.philkes.notallyx.data.imports.ExternalImporter +import com.philkes.notallyx.data.imports.ImportException +import com.philkes.notallyx.data.imports.ImportProgress +import com.philkes.notallyx.data.imports.ImportStage +import com.philkes.notallyx.data.imports.markdown.parseBodyAndSpansFromMarkdown +import com.philkes.notallyx.data.model.Audio +import com.philkes.notallyx.data.model.BaseNote +import com.philkes.notallyx.data.model.FileAttachment +import com.philkes.notallyx.data.model.Folder +import com.philkes.notallyx.data.model.ListItem +import com.philkes.notallyx.data.model.NoteViewMode +import com.philkes.notallyx.data.model.Reminder +import com.philkes.notallyx.data.model.Type +import com.philkes.notallyx.utils.moveAllFiles +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.util.Date +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import kotlin.collections.forEach +import kotlin.collections.map +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.json.Json + +@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) +class QuillpadImporter : ExternalImporter { + + internal val json = Json { + ignoreUnknownKeys = true + isLenient = true + allowTrailingComma = true + } + + override fun import( + app: Application, + source: Uri, + destination: File, + progress: MutableLiveData?, + ): Pair, File> { + progress?.postValue(ImportProgress(indeterminate = true, stage = ImportStage.EXTRACT_FILES)) + val dataFolder = + try { + app.contentResolver.openInputStream(source)!!.use { unzip(destination, it) } + } catch (e: Exception) { + throw ImportException(R.string.invalid_quillpad, e) + } + + val mediaFolder = File(dataFolder, "media") + if (mediaFolder.exists() && mediaFolder.isDirectory) { + mediaFolder.moveAllFiles(dataFolder) + } + + val backupFile = File(dataFolder, "backup.json") + if (!backupFile.exists()) { + throw ImportException( + R.string.invalid_quillpad, + RuntimeException("backup.json not found in ZIP"), + ) + } + + val quillpadBackup = + try { + json.decodeFromString(backupFile.readText()) + } catch (e: Exception) { + throw ImportException(R.string.invalid_quillpad, e) + } + + val notebookMap = quillpadBackup.notebooks.associate { it.id to it.name } + val noteReminders = quillpadBackup.reminders.groupBy { it.noteId } + val noteTags = quillpadBackup.joins.groupBy { it.noteId } + val tagMap = quillpadBackup.tags.associate { it.id to it.name } + + val total = quillpadBackup.notes.size + progress?.postValue(ImportProgress(0, total, stage = ImportStage.IMPORT_NOTES)) + var counter = 1 + + val baseNotes = + quillpadBackup.notes.map { quillpadNote -> + val result = quillpadNote.toBaseNote(notebookMap) + progress?.postValue( + ImportProgress(counter++, total, stage = ImportStage.IMPORT_NOTES) + ) + result + } + + return Pair(baseNotes, dataFolder) + } + + fun QuillpadNote.toBaseNote(notebookMap: Map): BaseNote { + val (body, spans) = + if (!isList && content != null) { + parseBodyAndSpansFromMarkdown(content) + } else { + Pair("", emptyList()) + } + + val items = + taskList?.mapIndexed { index, task -> + ListItem( + body = task.content, + checked = task.isDone, + isChild = false, + order = index, + children = mutableListOf(), + ) + } ?: emptyList() + + val images = mutableListOf() + val files = mutableListOf() + val audios = mutableListOf