mirror of
https://github.com/PhilKes/NotallyX.git
synced 2025-06-28 12:19:55 +00:00
Show Import progress as Dialog
This commit is contained in:
parent
df8d91e0f6
commit
014b4f5f11
39 changed files with 457 additions and 231 deletions
|
@ -1,5 +1,4 @@
|
|||
import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask
|
||||
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
|
@ -120,8 +119,11 @@ dependencies {
|
|||
implementation "com.github.bumptech.glide:glide:4.15.1"
|
||||
implementation "com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0"
|
||||
|
||||
implementation "com.google.code.findbugs:jsr305:3.0.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
||||
implementation "org.simpleframework:simple-xml:2.7.1"
|
||||
implementation("org.simpleframework:simple-xml:2.7.1") {
|
||||
exclude group: 'xpp3', module: 'xpp3'
|
||||
}
|
||||
implementation 'org.jsoup:jsoup:1.18.1'
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
|
|
25
app/proguard-rules.pro
vendored
25
app/proguard-rules.pro
vendored
|
@ -22,5 +22,28 @@
|
|||
-keep class ** extends androidx.navigation.Navigator
|
||||
-keep class ** implements org.ocpsoft.prettytime.TimeUnit
|
||||
|
||||
# SQLCipher
|
||||
-keep class net.sqlcipher.** { *; }
|
||||
-keep class net.sqlcipher.database.** { *; }
|
||||
-keep class net.sqlcipher.database.** { *; }
|
||||
|
||||
# SimpleXML
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation
|
||||
-keep interface org.simpleframework.xml.core.Label {
|
||||
public *;
|
||||
}
|
||||
-keep class * implements org.simpleframework.xml.core.Label {
|
||||
public *;
|
||||
}
|
||||
-keep interface org.simpleframework.xml.core.Parameter {
|
||||
public *;
|
||||
}
|
||||
-keep class * implements org.simpleframework.xml.core.Parameter {
|
||||
public *;
|
||||
}
|
||||
-keep interface org.simpleframework.xml.core.Extractor {
|
||||
public *;
|
||||
}
|
||||
-keep class * implements org.simpleframework.xml.core.Extractor {
|
||||
public *;
|
||||
}
|
|
@ -2,10 +2,21 @@ package com.philkes.notallyx.data.imports
|
|||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import java.io.File
|
||||
|
||||
interface ExternalImporter {
|
||||
|
||||
fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File>
|
||||
/**
|
||||
* Parses [BaseNote]s from [source] and copies attached files/images/audios to [destination]
|
||||
*
|
||||
* @return List of [BaseNote]s to import + folder containing attached files
|
||||
*/
|
||||
fun import(
|
||||
app: Application,
|
||||
source: Uri,
|
||||
destination: File,
|
||||
progress: MutableLiveData<ImportProgress>? = null,
|
||||
): Pair<List<BaseNote>, File>
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package com.philkes.notallyx.data.imports
|
||||
|
||||
class ImportException(val textResId: Int, cause: Throwable? = null) : RuntimeException(cause)
|
||||
class ImportException(val textResId: Int, cause: Throwable) : RuntimeException(cause)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.philkes.notallyx.data.imports
|
||||
|
||||
import com.philkes.notallyx.presentation.view.misc.Progress
|
||||
|
||||
open class ImportProgress(
|
||||
current: Int = 0,
|
||||
total: Int = 0,
|
||||
inProgress: Boolean = true,
|
||||
indeterminate: Boolean = false,
|
||||
val stage: ImportStage = ImportStage.IMPORT_NOTES,
|
||||
) : Progress(current, total, inProgress, indeterminate)
|
||||
|
||||
enum class ImportStage {
|
||||
IMPORT_NOTES,
|
||||
EXTRACT_FILES,
|
||||
IMPORT_FILES,
|
||||
}
|
|
@ -4,45 +4,83 @@ import android.app.Application
|
|||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.DataUtil
|
||||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter
|
||||
import com.philkes.notallyx.data.imports.google.GoogleKeepImporter
|
||||
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.Label
|
||||
import com.philkes.notallyx.presentation.viewmodel.NotallyModel
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class NotesImporter(private val app: Application, private val database: NotallyDatabase) {
|
||||
|
||||
suspend fun import(uri: Uri, importSource: ImportSource) {
|
||||
val (notes, importDataFolder) =
|
||||
when (importSource) {
|
||||
ImportSource.GOOGLE_KEEP -> GoogleKeepImporter().importFrom(uri, app)
|
||||
ImportSource.EVERNOTE -> EvernoteImporter().importFrom(uri, app)
|
||||
}
|
||||
database.getLabelDao().insert(notes.flatMap { it.labels }.distinct().map { Label(it) })
|
||||
importFiles(
|
||||
notes.flatMap { it.files }.distinct(),
|
||||
importDataFolder,
|
||||
NotallyModel.FileType.ANY,
|
||||
)
|
||||
importFiles(
|
||||
notes.flatMap { it.images }.distinct(),
|
||||
importDataFolder,
|
||||
NotallyModel.FileType.IMAGE,
|
||||
)
|
||||
importAudios(notes.flatMap { it.audios }.distinct(), importDataFolder)
|
||||
database.getBaseNoteDao().insert(notes)
|
||||
suspend fun import(
|
||||
uri: Uri,
|
||||
importSource: ImportSource,
|
||||
progress: MutableLiveData<ImportProgress>? = null,
|
||||
): Int {
|
||||
val tempDir = File(app.cacheDir, IMPORT_CACHE_FOLDER)
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs()
|
||||
}
|
||||
try {
|
||||
val (notes, importDataFolder) =
|
||||
try {
|
||||
when (importSource) {
|
||||
ImportSource.GOOGLE_KEEP -> GoogleKeepImporter()
|
||||
ImportSource.EVERNOTE -> EvernoteImporter()
|
||||
}.import(app, uri, tempDir, progress)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "import: failed", e)
|
||||
progress?.postValue(ImportProgress(inProgress = false))
|
||||
throw e
|
||||
}
|
||||
database.getLabelDao().insert(notes.flatMap { it.labels }.distinct().map { Label(it) })
|
||||
val files = notes.flatMap { it.files }.distinct()
|
||||
val images = notes.flatMap { it.images }.distinct()
|
||||
val audios = notes.flatMap { it.audios }.distinct()
|
||||
val totalFiles = files.size + images.size + audios.size
|
||||
val counter = AtomicInteger(1)
|
||||
progress?.postValue(
|
||||
ImportProgress(total = totalFiles, stage = ImportStage.IMPORT_FILES)
|
||||
)
|
||||
importFiles(
|
||||
files,
|
||||
importDataFolder,
|
||||
NotallyModel.FileType.ANY,
|
||||
progress,
|
||||
totalFiles,
|
||||
counter,
|
||||
)
|
||||
importFiles(
|
||||
images,
|
||||
importDataFolder,
|
||||
NotallyModel.FileType.IMAGE,
|
||||
progress,
|
||||
totalFiles,
|
||||
counter,
|
||||
)
|
||||
importAudios(audios, importDataFolder, progress, totalFiles, counter)
|
||||
database.getBaseNoteDao().insert(notes)
|
||||
progress?.postValue(ImportProgress(inProgress = false))
|
||||
return notes.size
|
||||
} finally {
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importFiles(
|
||||
files: List<FileAttachment>,
|
||||
sourceFolder: File,
|
||||
fileType: NotallyModel.FileType,
|
||||
progress: MutableLiveData<ImportProgress>?,
|
||||
total: Int?,
|
||||
counter: AtomicInteger?,
|
||||
) {
|
||||
files.forEach { file ->
|
||||
val uri = File(sourceFolder, file.localName).toUri()
|
||||
|
@ -55,27 +93,47 @@ class NotesImporter(private val app: Application, private val database: NotallyD
|
|||
file.originalName = fileAttachment.originalName
|
||||
file.mimeType = fileAttachment.mimeType
|
||||
}
|
||||
error?.let { Log.d(TAG, "Failed to import: $error") }
|
||||
error?.let { Log.e(TAG, "Failed to import: $error") }
|
||||
progress?.postValue(
|
||||
ImportProgress(
|
||||
current = counter!!.getAndIncrement(),
|
||||
total = total!!,
|
||||
stage = ImportStage.IMPORT_FILES,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importAudios(audios: List<Audio>, sourceFolder: File) {
|
||||
private suspend fun importAudios(
|
||||
audios: List<Audio>,
|
||||
sourceFolder: File,
|
||||
progress: MutableLiveData<ImportProgress>?,
|
||||
totalFiles: Int,
|
||||
counter: AtomicInteger,
|
||||
) {
|
||||
audios.forEach { originalAudio ->
|
||||
val file = File(sourceFolder, originalAudio.name)
|
||||
val audio = DataUtil.addAudio(app, file, false)
|
||||
originalAudio.name = audio.name
|
||||
originalAudio.duration = if (audio.duration == 0L) null else audio.duration
|
||||
originalAudio.timestamp = audio.timestamp
|
||||
progress?.postValue(
|
||||
ImportProgress(
|
||||
current = counter.getAndIncrement(),
|
||||
total = totalFiles,
|
||||
stage = ImportStage.IMPORT_FILES,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NotesImporter"
|
||||
const val IMPORT_CACHE_FOLDER = "imports"
|
||||
}
|
||||
}
|
||||
|
||||
enum class ImportSource(
|
||||
val folderName: String,
|
||||
val displayNameResId: Int,
|
||||
val mimeType: String,
|
||||
val helpTextResId: Int,
|
||||
|
@ -83,7 +141,6 @@ enum class ImportSource(
|
|||
val iconResId: Int,
|
||||
) {
|
||||
GOOGLE_KEEP(
|
||||
"googlekeep",
|
||||
R.string.google_keep,
|
||||
"application/zip",
|
||||
R.string.google_keep_help,
|
||||
|
@ -91,7 +148,6 @@ enum class ImportSource(
|
|||
R.drawable.icon_google_keep,
|
||||
),
|
||||
EVERNOTE(
|
||||
"evernote",
|
||||
R.string.evernote,
|
||||
"*/*", // 'application/enex+xml' is not recognized
|
||||
R.string.evernote_help,
|
||||
|
@ -99,11 +155,3 @@ enum class ImportSource(
|
|||
R.drawable.icon_evernote,
|
||||
),
|
||||
}
|
||||
|
||||
data class NotesImport(
|
||||
val baseNotes: List<BaseNote>,
|
||||
val labels: List<Label>,
|
||||
val files: List<FileAttachment>,
|
||||
val images: List<FileAttachment>,
|
||||
val audios: List<Audio>,
|
||||
)
|
||||
|
|
|
@ -4,10 +4,12 @@ import android.app.Application
|
|||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.webkit.MimeTypeMap
|
||||
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.ImportSource
|
||||
import com.philkes.notallyx.data.imports.ImportProgress
|
||||
import com.philkes.notallyx.data.imports.ImportStage
|
||||
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter.Companion.parseTimestamp
|
||||
import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml
|
||||
import com.philkes.notallyx.data.model.Audio
|
||||
|
@ -18,6 +20,7 @@ import com.philkes.notallyx.data.model.Folder
|
|||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.utils.IO.write
|
||||
import com.philkes.notallyx.utils.Operations
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.text.SimpleDateFormat
|
||||
|
@ -31,25 +34,38 @@ import org.simpleframework.xml.core.Persister
|
|||
class EvernoteImporter : ExternalImporter {
|
||||
private val serializer: Serializer = Persister(AnnotationStrategy())
|
||||
|
||||
override fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File> {
|
||||
if (MimeTypeMap.getFileExtensionFromUrl(uri.toString()) != "enex") {
|
||||
throw ImportException(R.string.invalid_evernote)
|
||||
}
|
||||
val tempDir = File(app.cacheDir, ImportSource.EVERNOTE.folderName)
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs()
|
||||
override fun import(
|
||||
app: Application,
|
||||
source: Uri,
|
||||
destination: File,
|
||||
progress: MutableLiveData<ImportProgress>?,
|
||||
): Pair<List<BaseNote>, File> {
|
||||
progress?.postValue(ImportProgress(indeterminate = true))
|
||||
if (MimeTypeMap.getFileExtensionFromUrl(source.toString()) != "enex") {
|
||||
throw ImportException(
|
||||
R.string.invalid_evernote,
|
||||
IllegalArgumentException("Provided file is not in ENEX format"),
|
||||
)
|
||||
}
|
||||
val evernoteExport: EvernoteExport =
|
||||
parseExport(app.contentResolver.openInputStream(uri)!!)!!
|
||||
saveResourcesToFiles(
|
||||
evernoteExport.notes.flatMap { it.resources }.distinctBy { it.attributes?.fileName },
|
||||
tempDir,
|
||||
)
|
||||
parseExport(app.contentResolver.openInputStream(source)!!)!!
|
||||
|
||||
val total = evernoteExport.notes.size
|
||||
progress?.postValue(ImportProgress(total = total))
|
||||
var counter = 1
|
||||
try {
|
||||
val notes = evernoteExport.notes.map { it.mapToBaseNote() }
|
||||
return Pair(notes, tempDir)
|
||||
val notes =
|
||||
evernoteExport.notes.map {
|
||||
val note = it.mapToBaseNote()
|
||||
progress?.postValue(ImportProgress(current = counter++, total = total))
|
||||
note
|
||||
}
|
||||
val resources =
|
||||
evernoteExport.notes.flatMap { it.resources }.distinctBy { it.attributes?.fileName }
|
||||
saveResourcesToFiles(app, resources, destination, progress)
|
||||
return Pair(notes, destination)
|
||||
} catch (e: Exception) {
|
||||
throw ImportException(R.string.invalid_evernote)
|
||||
throw ImportException(R.string.invalid_evernote, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,11 +76,30 @@ class EvernoteImporter : ExternalImporter {
|
|||
throw ImportException(R.string.invalid_evernote, e)
|
||||
}
|
||||
|
||||
private fun saveResourcesToFiles(resources: Collection<EvernoteResource>, dir: File) {
|
||||
resources.forEach {
|
||||
private fun saveResourcesToFiles(
|
||||
app: Application,
|
||||
resources: Collection<EvernoteResource>,
|
||||
dir: File,
|
||||
progress: MutableLiveData<ImportProgress>? = null,
|
||||
) {
|
||||
progress?.postValue(
|
||||
ImportProgress(total = resources.size, stage = ImportStage.EXTRACT_FILES)
|
||||
)
|
||||
resources.forEachIndexed { idx, it ->
|
||||
val file = File(dir, it.attributes!!.fileName)
|
||||
val data = Base64.decode(it.data!!.content.trimStart(), Base64.DEFAULT)
|
||||
file.write(data)
|
||||
try {
|
||||
val data = Base64.decode(it.data!!.content.trimStart(), Base64.DEFAULT)
|
||||
file.write(data)
|
||||
} catch (e: Exception) {
|
||||
Operations.log(app, e)
|
||||
}
|
||||
progress?.postValue(
|
||||
ImportProgress(
|
||||
current = idx + 1,
|
||||
total = resources.size,
|
||||
stage = ImportStage.EXTRACT_FILES,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,12 @@ package com.philkes.notallyx.data.imports.google
|
|||
|
||||
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.ImportSource
|
||||
import com.philkes.notallyx.data.imports.ImportProgress
|
||||
import com.philkes.notallyx.data.imports.ImportStage
|
||||
import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml
|
||||
import com.philkes.notallyx.data.model.Audio
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
|
@ -32,58 +34,72 @@ class GoogleKeepImporter : ExternalImporter {
|
|||
allowTrailingComma = true
|
||||
}
|
||||
|
||||
override fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File> {
|
||||
val tempDir = File(app.cacheDir, ImportSource.GOOGLE_KEEP.folderName)
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs()
|
||||
}
|
||||
override fun import(
|
||||
app: Application,
|
||||
source: Uri,
|
||||
destination: File,
|
||||
progress: MutableLiveData<ImportProgress>?,
|
||||
): Pair<List<BaseNote>, File> {
|
||||
progress?.postValue(ImportProgress(indeterminate = true, stage = ImportStage.EXTRACT_FILES))
|
||||
val dataFolder =
|
||||
try {
|
||||
unzip(tempDir, app.contentResolver.openInputStream(uri)!!)
|
||||
unzip(destination, app.contentResolver.openInputStream(source)!!)
|
||||
} catch (e: Exception) {
|
||||
throw ImportException(R.string.invalid_google_keep, e)
|
||||
}
|
||||
|
||||
if (!dataFolder.exists()) {
|
||||
throw ImportException(R.string.invalid_google_keep)
|
||||
throw ImportException(
|
||||
R.string.invalid_google_keep,
|
||||
RuntimeException("Extracting Takeout.zip failed"),
|
||||
)
|
||||
}
|
||||
|
||||
val baseNotes =
|
||||
val noteFiles =
|
||||
dataFolder
|
||||
.walk()
|
||||
.mapNotNull { importFile ->
|
||||
if (importFile.extension == "json") {
|
||||
parseToBaseNote(importFile.readText())
|
||||
} else null
|
||||
.listFiles { file ->
|
||||
file.isFile && file.extension.equals("json", ignoreCase = true)
|
||||
}
|
||||
?.toList() ?: emptyList()
|
||||
val total = noteFiles.size
|
||||
progress?.postValue(ImportProgress(0, total, stage = ImportStage.IMPORT_NOTES))
|
||||
var counter = 1
|
||||
val baseNotes =
|
||||
noteFiles
|
||||
.mapNotNull { file ->
|
||||
val baseNote = file.readText().parseToBaseNote()
|
||||
progress?.postValue(
|
||||
ImportProgress(counter++, total, stage = ImportStage.IMPORT_NOTES)
|
||||
)
|
||||
baseNote
|
||||
}
|
||||
.toList()
|
||||
return Pair(baseNotes, dataFolder)
|
||||
}
|
||||
|
||||
fun parseToBaseNote(jsonString: String): BaseNote {
|
||||
val keepNote = json.decodeFromString<KeepNote>(jsonString)
|
||||
|
||||
fun String.parseToBaseNote(): BaseNote {
|
||||
val googleKeepNote = json.decodeFromString<GoogleKeepNote>(this)
|
||||
val (body, spans) =
|
||||
parseBodyAndSpansFromHtml(
|
||||
keepNote.textContentHtml,
|
||||
googleKeepNote.textContentHtml,
|
||||
paragraphsAsNewLine = true,
|
||||
brTagsAsNewLine = true,
|
||||
)
|
||||
|
||||
val images =
|
||||
keepNote.attachments
|
||||
googleKeepNote.attachments
|
||||
.filter { it.mimetype.startsWith("image") }
|
||||
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
|
||||
val files =
|
||||
keepNote.attachments
|
||||
googleKeepNote.attachments
|
||||
.filter { !it.mimetype.startsWith("audio") && !it.mimetype.startsWith("image") }
|
||||
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
|
||||
val audios =
|
||||
keepNote.attachments
|
||||
googleKeepNote.attachments
|
||||
.filter { it.mimetype.startsWith("audio") }
|
||||
.map { Audio(it.filePath, 0L, System.currentTimeMillis()) }
|
||||
val items =
|
||||
keepNote.listContent.mapIndexed { index, item ->
|
||||
googleKeepNote.listContent.mapIndexed { index, item ->
|
||||
ListItem(
|
||||
body = item.text,
|
||||
checked = item.isChecked,
|
||||
|
@ -95,19 +111,19 @@ class GoogleKeepImporter : ExternalImporter {
|
|||
|
||||
return BaseNote(
|
||||
id = 0L, // Auto-generated
|
||||
type = if (keepNote.listContent.isNotEmpty()) Type.LIST else Type.NOTE,
|
||||
type = if (googleKeepNote.listContent.isNotEmpty()) Type.LIST else Type.NOTE,
|
||||
folder =
|
||||
when {
|
||||
keepNote.isTrashed -> Folder.DELETED
|
||||
keepNote.isArchived -> Folder.ARCHIVED
|
||||
googleKeepNote.isTrashed -> Folder.DELETED
|
||||
googleKeepNote.isArchived -> Folder.ARCHIVED
|
||||
else -> Folder.NOTES
|
||||
},
|
||||
color = Color.DEFAULT, // Ignoring color mapping
|
||||
title = keepNote.title,
|
||||
pinned = keepNote.isPinned,
|
||||
timestamp = keepNote.createdTimestampUsec / 1000,
|
||||
modifiedTimestamp = keepNote.userEditedTimestampUsec / 1000,
|
||||
labels = keepNote.labels.map { it.name },
|
||||
title = googleKeepNote.title,
|
||||
pinned = googleKeepNote.isPinned,
|
||||
timestamp = googleKeepNote.createdTimestampUsec / 1000,
|
||||
modifiedTimestamp = googleKeepNote.userEditedTimestampUsec / 1000,
|
||||
labels = googleKeepNote.labels.map { it.name },
|
||||
body = body,
|
||||
spans = spans,
|
||||
items = items,
|
||||
|
|
|
@ -4,8 +4,8 @@ import com.philkes.notallyx.data.model.Color
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class KeepNote(
|
||||
val attachments: List<KeepAttachment> = listOf(),
|
||||
data class GoogleKeepNote(
|
||||
val attachments: List<GoogleKeepAttachment> = listOf(),
|
||||
val color: String = Color.DEFAULT.name,
|
||||
val isTrashed: Boolean = false,
|
||||
val isArchived: Boolean = false,
|
||||
|
@ -13,14 +13,14 @@ data class KeepNote(
|
|||
val textContent: String = "",
|
||||
val textContentHtml: String = "",
|
||||
val title: String = "",
|
||||
val labels: List<KeepLabel> = listOf(),
|
||||
val labels: List<GoogleKeepLabel> = listOf(),
|
||||
val userEditedTimestampUsec: Long = System.currentTimeMillis(),
|
||||
val createdTimestampUsec: Long = System.currentTimeMillis(),
|
||||
val listContent: List<KeepListItem> = listOf(),
|
||||
val listContent: List<GoogleKeepListItem> = listOf(),
|
||||
)
|
||||
|
||||
@Serializable data class KeepLabel(val name: String)
|
||||
@Serializable data class GoogleKeepLabel(val name: String)
|
||||
|
||||
@Serializable data class KeepAttachment(val filePath: String, val mimetype: String)
|
||||
@Serializable data class GoogleKeepAttachment(val filePath: String, val mimetype: String)
|
||||
|
||||
@Serializable data class KeepListItem(val text: String, val isChecked: Boolean)
|
||||
@Serializable data class GoogleKeepListItem(val text: String, val isChecked: Boolean)
|
|
@ -48,6 +48,8 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.imports.ImportProgress
|
||||
import com.philkes.notallyx.data.imports.ImportStage
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.SpanRepresentation
|
||||
import com.philkes.notallyx.data.model.getUrl
|
||||
|
@ -279,7 +281,7 @@ fun View.getString(id: Int, vararg formatArgs: String): String {
|
|||
}
|
||||
|
||||
fun View.getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any): String {
|
||||
return context.resources.getQuantityString(id, quantity, *formatArgs)
|
||||
return context.getQuantityString(id, quantity, *formatArgs)
|
||||
}
|
||||
|
||||
fun Folder.movedToResId(): Int {
|
||||
|
@ -370,11 +372,33 @@ fun MutableLiveData<out Progress>.setupProgressDialog(fragment: Fragment, titleI
|
|||
)
|
||||
}
|
||||
|
||||
private fun MutableLiveData<out Progress>.setupProgressDialog(
|
||||
fun MutableLiveData<ImportProgress>.setupImportProgressDialog(fragment: Fragment, titleId: Int) {
|
||||
setupProgressDialog(
|
||||
fragment.requireContext(),
|
||||
fragment.layoutInflater,
|
||||
fragment.viewLifecycleOwner,
|
||||
titleId,
|
||||
) { context, binding, progress ->
|
||||
val stageStr =
|
||||
context.getString(
|
||||
when (progress.stage) {
|
||||
ImportStage.IMPORT_NOTES -> R.string.imported_notes
|
||||
ImportStage.EXTRACT_FILES -> R.string.extracted_files
|
||||
ImportStage.IMPORT_FILES -> R.string.imported_files
|
||||
}
|
||||
)
|
||||
binding.Count.text =
|
||||
"${context.getString(R.string.count, progress.current, progress.total)} $stageStr"
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Progress> MutableLiveData<T>.setupProgressDialog(
|
||||
context: Context,
|
||||
layoutInflater: LayoutInflater,
|
||||
viewLifecycleOwner: LifecycleOwner,
|
||||
titleId: Int,
|
||||
renderProgress: ((context: Context, binding: DialogProgressBinding, progress: T) -> Unit)? =
|
||||
null,
|
||||
) {
|
||||
val dialogBinding = DialogProgressBinding.inflate(layoutInflater)
|
||||
val dialog =
|
||||
|
@ -398,7 +422,10 @@ private fun MutableLiveData<out Progress>.setupProgressDialog(
|
|||
max = progress.total
|
||||
setProgressCompat(progress.current, true)
|
||||
}
|
||||
Count.text = context.getString(R.string.count, progress.current, progress.total)
|
||||
if (renderProgress == null) {
|
||||
Count.text =
|
||||
context.getString(R.string.count, progress.current, progress.total)
|
||||
} else renderProgress.invoke(context, this, progress)
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
|
@ -455,3 +482,7 @@ fun MaterialAlertDialogBuilder.showAndFocus(view: View): AlertDialog {
|
|||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun Context.getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any): String {
|
||||
return resources.getQuantityString(id, quantity, quantity, *formatArgs)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import com.philkes.notallyx.presentation.activity.note.EditListActivity
|
|||
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
|
||||
import com.philkes.notallyx.presentation.add
|
||||
import com.philkes.notallyx.presentation.applySpans
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.movedToResId
|
||||
import com.philkes.notallyx.presentation.view.main.ColorAdapter
|
||||
import com.philkes.notallyx.presentation.view.misc.MenuDialog
|
||||
|
@ -217,7 +218,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
val ids = model.moveBaseNotes(folderTo)
|
||||
Snackbar.make(
|
||||
findViewById(R.id.DrawerLayout),
|
||||
resources.getQuantityString(folderTo.movedToResId(), ids.size, ids.size),
|
||||
getQuantityString(folderTo.movedToResId(), ids.size),
|
||||
Snackbar.LENGTH_SHORT,
|
||||
)
|
||||
.apply { setAction(R.string.undo) { model.moveBaseNotes(ids, folderFrom) } }
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.FO
|
|||
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.NOTE_ID
|
||||
import com.philkes.notallyx.presentation.activity.note.EditListActivity
|
||||
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.movedToResId
|
||||
import com.philkes.notallyx.presentation.view.Constants
|
||||
import com.philkes.notallyx.presentation.view.main.BaseNoteAdapter
|
||||
|
@ -103,7 +104,7 @@ abstract class NotallyFragment : Fragment(), ListItemListener {
|
|||
val folderTo = Folder.valueOf(data.getStringExtra(FOLDER_TO)!!)
|
||||
Snackbar.make(
|
||||
binding!!.root,
|
||||
resources.getQuantityString(folderTo.movedToResId(), 1, 1),
|
||||
requireContext().getQuantityString(folderTo.movedToResId(), 1),
|
||||
Snackbar.LENGTH_SHORT,
|
||||
)
|
||||
.apply {
|
||||
|
|
|
@ -31,6 +31,7 @@ import com.philkes.notallyx.databinding.PreferenceSeekbarBinding
|
|||
import com.philkes.notallyx.databinding.TextInputDialogBinding
|
||||
import com.philkes.notallyx.presentation.canAuthenticateWithBiometrics
|
||||
import com.philkes.notallyx.presentation.checkedTag
|
||||
import com.philkes.notallyx.presentation.setupImportProgressDialog
|
||||
import com.philkes.notallyx.presentation.setupProgressDialog
|
||||
import com.philkes.notallyx.presentation.view.misc.AutoBackup
|
||||
import com.philkes.notallyx.presentation.view.misc.AutoBackupMax
|
||||
|
@ -124,7 +125,7 @@ class SettingsFragment : Fragment() {
|
|||
binding.ClearData.setOnClickListener { clearData() }
|
||||
|
||||
model.exportProgress.setupProgressDialog(this, R.string.exporting_backup)
|
||||
model.importProgress.setupProgressDialog(this, R.string.importing_backup)
|
||||
model.importProgress.setupImportProgressDialog(this, R.string.importing_backup)
|
||||
model.deletionProgress.setupProgressDialog(this, R.string.deleting_files)
|
||||
|
||||
binding.GitHub.setOnClickListener { openLink("https://github.com/PhilKes/NotallyX") }
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.philkes.notallyx.databinding.ActivityEditBinding
|
|||
import com.philkes.notallyx.presentation.activity.LockedActivity
|
||||
import com.philkes.notallyx.presentation.add
|
||||
import com.philkes.notallyx.presentation.displayFormattedTimestamp
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.setupProgressDialog
|
||||
import com.philkes.notallyx.presentation.view.Constants
|
||||
import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByCreationDate
|
||||
|
@ -518,7 +519,7 @@ abstract class EditActivity(private val type: Type) : LockedActivity<ActivityEdi
|
|||
} else {
|
||||
R.plurals.cant_add_files
|
||||
}
|
||||
val title = resources.getQuantityString(message, errors.size, errors.size)
|
||||
val title = getQuantityString(message, errors.size)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setView(recyclerView)
|
||||
|
|
|
@ -15,7 +15,6 @@ import android.view.ActionMode
|
|||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.core.util.PatternsCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.philkes.notallyx.data.dao.BaseNoteDao
|
|||
import com.philkes.notallyx.data.dao.CommonDao
|
||||
import com.philkes.notallyx.data.dao.LabelDao
|
||||
import com.philkes.notallyx.data.imports.ImportException
|
||||
import com.philkes.notallyx.data.imports.ImportProgress
|
||||
import com.philkes.notallyx.data.imports.ImportSource
|
||||
import com.philkes.notallyx.data.imports.NotesImporter
|
||||
import com.philkes.notallyx.data.model.Attachment
|
||||
|
@ -36,6 +37,7 @@ import com.philkes.notallyx.data.model.Label
|
|||
import com.philkes.notallyx.data.model.SearchResult
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.presentation.applySpans
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.view.misc.AutoBackup
|
||||
import com.philkes.notallyx.presentation.view.misc.ListInfo
|
||||
import com.philkes.notallyx.presentation.view.misc.Progress
|
||||
|
@ -109,7 +111,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
val fileRoot = app.getExternalFilesDirectory()
|
||||
private val audioRoot = app.getExternalAudioDirectory()
|
||||
|
||||
val importProgress = MutableLiveData<Progress>()
|
||||
val importProgress = MutableLiveData<ImportProgress>()
|
||||
val exportProgress = MutableLiveData<Progress>()
|
||||
val deletionProgress = MutableLiveData<Progress>()
|
||||
|
||||
|
@ -219,8 +221,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
}
|
||||
|
||||
fun importZipBackup(uri: Uri, password: String) {
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Operations.log(app, throwable)
|
||||
Toast.makeText(app, R.string.invalid_backup, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
val backupDir = app.getBackupDir()
|
||||
viewModelScope.launch { importZip(app, uri, backupDir, password, importProgress) }
|
||||
viewModelScope.launch(exceptionHandler) {
|
||||
importZip(app, uri, backupDir, password, importProgress)
|
||||
}
|
||||
}
|
||||
|
||||
fun importXmlBackup(uri: Uri) {
|
||||
|
@ -230,12 +239,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
}
|
||||
|
||||
viewModelScope.launch(exceptionHandler) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val stream = requireNotNull(app.contentResolver.openInputStream(uri))
|
||||
val backup = XMLUtils.readBackupFromStream(stream)
|
||||
commonDao.importBackup(backup.first, backup.second)
|
||||
}
|
||||
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show()
|
||||
val importedNotes =
|
||||
withContext(Dispatchers.IO) {
|
||||
val stream = requireNotNull(app.contentResolver.openInputStream(uri))
|
||||
val (baseNotes, labels) = XMLUtils.readBackupFromStream(stream)
|
||||
commonDao.importBackup(baseNotes, labels)
|
||||
baseNotes.size
|
||||
}
|
||||
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
|
||||
Toast.makeText(app, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,8 +266,12 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
Operations.log(app, throwable)
|
||||
}
|
||||
viewModelScope.launch(exceptionHandler) {
|
||||
withContext(Dispatchers.IO) { NotesImporter(app, database).import(uri, importSource) }
|
||||
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show()
|
||||
val importedNotes =
|
||||
withContext(Dispatchers.IO) {
|
||||
NotesImporter(app, database).import(uri, importSource, importProgress)
|
||||
}
|
||||
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
|
||||
Toast.makeText(app, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import com.philkes.notallyx.R
|
|||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.presentation.displayFormattedTimestamp
|
||||
import com.philkes.notallyx.presentation.view.misc.TextSize
|
||||
import com.philkes.notallyx.presentation.widget.WidgetProvider.Companion.getSelectNoteIntent
|
||||
|
||||
|
|
|
@ -2,4 +2,8 @@ package com.philkes.notallyx.utils
|
|||
|
||||
import com.philkes.notallyx.presentation.viewmodel.NotallyModel
|
||||
|
||||
class FileError(val name: String, val description: String, val fileType: NotallyModel.FileType)
|
||||
data class FileError(
|
||||
val name: String,
|
||||
val description: String,
|
||||
val fileType: NotallyModel.FileType,
|
||||
)
|
||||
|
|
|
@ -6,9 +6,9 @@ import android.graphics.Bitmap
|
|||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.philkes.notallyx.data.model.Attachment
|
||||
import com.philkes.notallyx.data.model.Audio
|
||||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
|
@ -85,7 +85,7 @@ object IO {
|
|||
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
|
||||
mimeType != null && hasAudio != null ||
|
||||
duration != null // If it has audio metadata, it's a valid audio file
|
||||
duration != null // If it has audio metadata, it's a valid audio file
|
||||
} catch (e: Exception) {
|
||||
false // An exception means it’s not a valid audio file
|
||||
} finally {
|
||||
|
|
|
@ -9,6 +9,8 @@ import androidx.core.database.getLongOrNull
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.imports.ImportProgress
|
||||
import com.philkes.notallyx.data.imports.ImportStage
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Color
|
||||
import com.philkes.notallyx.data.model.Converters
|
||||
|
@ -16,7 +18,7 @@ import com.philkes.notallyx.data.model.FileAttachment
|
|||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.Label
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.presentation.view.misc.Progress
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.utils.IO.SUBFOLDER_AUDIOS
|
||||
import com.philkes.notallyx.utils.IO.SUBFOLDER_FILES
|
||||
import com.philkes.notallyx.utils.IO.SUBFOLDER_IMAGES
|
||||
|
@ -46,93 +48,115 @@ object Import {
|
|||
zipFileUri: Uri,
|
||||
databaseFolder: File,
|
||||
zipPassword: String,
|
||||
importingBackup: MutableLiveData<Progress>? = null,
|
||||
importingBackup: MutableLiveData<ImportProgress>? = null,
|
||||
) {
|
||||
importingBackup?.postValue(Progress(indeterminate = true))
|
||||
importingBackup?.postValue(ImportProgress(indeterminate = true))
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val stream = requireNotNull(app.contentResolver.openInputStream(zipFileUri))
|
||||
val tempZipFile = File(databaseFolder, "TEMP.zip")
|
||||
stream.copyToFile(tempZipFile)
|
||||
val zipFile = ZipFile(tempZipFile)
|
||||
if (zipFile.isEncrypted) {
|
||||
zipFile.setPassword(zipPassword.toCharArray())
|
||||
}
|
||||
zipFile.extractFile(
|
||||
NotallyDatabase.DatabaseName,
|
||||
databaseFolder.path,
|
||||
NotallyDatabase.DatabaseName,
|
||||
)
|
||||
|
||||
val database =
|
||||
SQLiteDatabase.openDatabase(
|
||||
File(databaseFolder, NotallyDatabase.DatabaseName).path,
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READONLY,
|
||||
)
|
||||
|
||||
val labelCursor = database.query("Label", null, null, null, null, null, null)
|
||||
val baseNoteCursor = database.query("BaseNote", null, null, null, null, null, null)
|
||||
|
||||
val labels = labelCursor.toList { cursor -> cursor.toLabel() }
|
||||
val baseNotes = baseNoteCursor.toList { cursor -> cursor.toBaseNote() }
|
||||
|
||||
delay(1000)
|
||||
|
||||
val total =
|
||||
baseNotes.fold(0) { acc, baseNote ->
|
||||
acc + baseNote.images.size + baseNote.files.size + baseNote.audios.size
|
||||
val importedNotes =
|
||||
withContext(Dispatchers.IO) {
|
||||
val stream = requireNotNull(app.contentResolver.openInputStream(zipFileUri))
|
||||
val tempZipFile = File(databaseFolder, "TEMP.zip")
|
||||
stream.copyToFile(tempZipFile)
|
||||
val zipFile = ZipFile(tempZipFile)
|
||||
if (zipFile.isEncrypted) {
|
||||
zipFile.setPassword(zipPassword.toCharArray())
|
||||
}
|
||||
importingBackup?.postValue(Progress(0, total))
|
||||
zipFile.extractFile(
|
||||
NotallyDatabase.DatabaseName,
|
||||
databaseFolder.path,
|
||||
NotallyDatabase.DatabaseName,
|
||||
)
|
||||
|
||||
val current = AtomicInteger(1)
|
||||
val imageRoot = app.getExternalImagesDirectory()
|
||||
val fileRoot = app.getExternalFilesDirectory()
|
||||
val audioRoot = app.getExternalAudioDirectory()
|
||||
baseNotes.forEach { baseNote ->
|
||||
importFiles(
|
||||
app,
|
||||
baseNote.images,
|
||||
SUBFOLDER_IMAGES,
|
||||
imageRoot,
|
||||
zipFile,
|
||||
current,
|
||||
total,
|
||||
val database =
|
||||
SQLiteDatabase.openDatabase(
|
||||
File(databaseFolder, NotallyDatabase.DatabaseName).path,
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READONLY,
|
||||
)
|
||||
|
||||
val labelCursor = database.query("Label", null, null, null, null, null, null)
|
||||
val baseNoteCursor =
|
||||
database.query("BaseNote", null, null, null, null, null, null)
|
||||
|
||||
val labels = labelCursor.toList { cursor -> cursor.toLabel() }
|
||||
|
||||
var total = baseNoteCursor.count
|
||||
var counter = 1
|
||||
importingBackup?.postValue(ImportProgress(0, total))
|
||||
val baseNotes =
|
||||
baseNoteCursor.toList { cursor ->
|
||||
val baseNote = cursor.toBaseNote()
|
||||
importingBackup?.postValue(ImportProgress(counter++, total))
|
||||
baseNote
|
||||
}
|
||||
|
||||
delay(1000)
|
||||
|
||||
total =
|
||||
baseNotes.fold(0) { acc, baseNote ->
|
||||
acc + baseNote.images.size + baseNote.files.size + baseNote.audios.size
|
||||
}
|
||||
importingBackup?.postValue(
|
||||
ImportProgress(0, total, stage = ImportStage.IMPORT_FILES)
|
||||
)
|
||||
importFiles(
|
||||
app,
|
||||
baseNote.files,
|
||||
SUBFOLDER_FILES,
|
||||
fileRoot,
|
||||
zipFile,
|
||||
current,
|
||||
total,
|
||||
)
|
||||
baseNote.audios.forEach { audio ->
|
||||
try {
|
||||
val audioFilePath = "$SUBFOLDER_AUDIOS/${audio.name}"
|
||||
val entry = zipFile.getFileHeader(audioFilePath)
|
||||
if (entry != null) {
|
||||
val name = "${UUID.randomUUID()}.m4a"
|
||||
zipFile.extractFile(audioFilePath, audioRoot!!.path, name)
|
||||
audio.name = name
|
||||
|
||||
val current = AtomicInteger(1)
|
||||
val imageRoot = app.getExternalImagesDirectory()
|
||||
val fileRoot = app.getExternalFilesDirectory()
|
||||
val audioRoot = app.getExternalAudioDirectory()
|
||||
baseNotes.forEach { baseNote ->
|
||||
importFiles(
|
||||
app,
|
||||
baseNote.images,
|
||||
SUBFOLDER_IMAGES,
|
||||
imageRoot,
|
||||
zipFile,
|
||||
current,
|
||||
total,
|
||||
importingBackup,
|
||||
)
|
||||
importFiles(
|
||||
app,
|
||||
baseNote.files,
|
||||
SUBFOLDER_FILES,
|
||||
fileRoot,
|
||||
zipFile,
|
||||
current,
|
||||
total,
|
||||
importingBackup,
|
||||
)
|
||||
baseNote.audios.forEach { audio ->
|
||||
try {
|
||||
val audioFilePath = "$SUBFOLDER_AUDIOS/${audio.name}"
|
||||
val entry = zipFile.getFileHeader(audioFilePath)
|
||||
if (entry != null) {
|
||||
val name = "${UUID.randomUUID()}.m4a"
|
||||
zipFile.extractFile(audioFilePath, audioRoot!!.path, name)
|
||||
audio.name = name
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Operations.log(app, exception)
|
||||
} finally {
|
||||
importingBackup?.postValue(
|
||||
ImportProgress(
|
||||
current.getAndIncrement(),
|
||||
total,
|
||||
stage = ImportStage.IMPORT_FILES,
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Operations.log(app, exception)
|
||||
} finally {
|
||||
importingBackup?.postValue(Progress(current.get(), total))
|
||||
current.getAndIncrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotallyDatabase.getDatabase(app)
|
||||
.value
|
||||
.getCommonDao()
|
||||
.importBackup(baseNotes, labels)
|
||||
}
|
||||
NotallyDatabase.getDatabase(app)
|
||||
.value
|
||||
.getCommonDao()
|
||||
.importBackup(baseNotes, labels)
|
||||
baseNotes.size
|
||||
}
|
||||
databaseFolder.clearDirectory()
|
||||
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show()
|
||||
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
|
||||
Toast.makeText(app, message, Toast.LENGTH_LONG).show()
|
||||
} catch (e: ZipException) {
|
||||
if (e.type == ZipException.Type.WRONG_PASSWORD) {
|
||||
Toast.makeText(app, R.string.wrong_password, Toast.LENGTH_LONG).show()
|
||||
|
@ -144,7 +168,7 @@ object Import {
|
|||
Toast.makeText(app, R.string.invalid_backup, Toast.LENGTH_LONG).show()
|
||||
Operations.log(app, e)
|
||||
} finally {
|
||||
importingBackup?.value = Progress(inProgress = false)
|
||||
importingBackup?.value = ImportProgress(inProgress = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,7 +180,7 @@ object Import {
|
|||
zipFile: ZipFile,
|
||||
current: AtomicInteger,
|
||||
total: Int,
|
||||
importingBackup: MutableLiveData<Progress>? = null,
|
||||
importingBackup: MutableLiveData<ImportProgress>? = null,
|
||||
) {
|
||||
files.forEach { file ->
|
||||
try {
|
||||
|
@ -170,8 +194,13 @@ object Import {
|
|||
} catch (exception: Exception) {
|
||||
Operations.log(app, exception)
|
||||
} finally {
|
||||
importingBackup?.postValue(Progress(current.get(), total))
|
||||
current.getAndIncrement()
|
||||
importingBackup?.postValue(
|
||||
ImportProgress(
|
||||
current.getAndIncrement(),
|
||||
total,
|
||||
stage = ImportStage.IMPORT_FILES,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,6 @@
|
|||
<string name="choose_another_folder">Vybrat jinou složku</string>
|
||||
<string name="cant_find_folder">Složka nebyla nalezena. Možná byla přesunuta nebo smazána.</string>
|
||||
<string name="invalid_backup">Neplatný soubor zálohy</string>
|
||||
<string name="imported_backup">Záloha importována</string>
|
||||
<string name="exporting_backup">Vytváří se záloha</string>
|
||||
<string name="importing_backup">Importuje se záloha</string>
|
||||
<string name="calculating">Probíhá výpočet…</string>
|
||||
|
|
|
@ -139,7 +139,6 @@
|
|||
<string name="cant_find_folder">Kann den Ordner nicht finden. Er kann verschoben oder gelöscht worden sein</string>
|
||||
<string name="invalid_backup">Ungültige Backup</string>
|
||||
<string name="google_keep_help">Um deine Notizen aus Google Notizen zu importieren musst du deine Google Takeout ZIP Datei herunterladen\n\nFalls du das Takeout ZIP schon hast, klicke auf Import und wähle es aus.</string>
|
||||
<string name="imported_backup">Backup importiert</string>
|
||||
<string name="exporting_backup">Backup Exportieren</string>
|
||||
<string name="importing_backup">Backup Importieren</string>
|
||||
<string name="calculating">Berechne…</string>
|
||||
|
|
|
@ -68,7 +68,6 @@
|
|||
|
||||
<!-- Messages -->
|
||||
<string name="invalid_backup">Copia de seguridad inválida</string>
|
||||
<string name="imported_backup">Copia de seguridad importada</string>
|
||||
<string name="install_an_email">Instale una aplicación de correo electrónico para enviar comentarios</string>
|
||||
<string name="install_a_browser">Instala un navegador para abrir este enlace</string>
|
||||
|
||||
|
|
|
@ -112,7 +112,6 @@
|
|||
<string name="choose_another_folder">Choisir un autre dossier</string>
|
||||
<string name="cant_find_folder">Impossible de trouver le dossier. Il a peut-être été déplacé ou supprimé.</string>
|
||||
<string name="invalid_backup">Sauvegarde invalide</string>
|
||||
<string name="imported_backup">Sauvegarde importée</string>
|
||||
<string name="exporting_backup">Exportation de la sauvegarde</string>
|
||||
<string name="importing_backup">Importation de la sauvegarde</string>
|
||||
<string name="calculating">Calcul…</string>
|
||||
|
|
|
@ -67,7 +67,6 @@
|
|||
<string name="empty_list">Daftar kosong</string>
|
||||
<string name="cant_open_link">Tidak bisa membuka tautan</string>
|
||||
<string name="invalid_backup">Cadangan tidak valid</string>
|
||||
<string name="imported_backup">Cadangan yang diimpor</string>
|
||||
<string name="something_went_wrong">Telah terjadi kesalahan. Silakan coba lagi.</string>
|
||||
<string name="install_a_browser">Pasang peramban untuk membuka tautan ini</string>
|
||||
|
||||
|
|
|
@ -68,7 +68,6 @@
|
|||
<string name="empty_list">Lista vuota</string>
|
||||
<string name="cant_open_link">Impossibile aprire il link</string>
|
||||
<string name="invalid_backup">Backup invalido</string>
|
||||
<string name="imported_backup">Backup importato</string>
|
||||
<string name="something_went_wrong">Qualcosa è andato storto. Per favore riprova.</string>
|
||||
|
||||
<!-- Settings Page -->
|
||||
|
|
|
@ -71,7 +71,6 @@
|
|||
<string name="cant_open_link">လင့်ကိုမဖွင့်နိုင်ပါ။</string>
|
||||
<string name="invalid_image">ရုပ်ပုံမှားနေသည်။</string>
|
||||
<string name="invalid_backup">အရန်သိမ်းဖိုင်မှားနေသည်။</string>
|
||||
<string name="imported_backup">အရန်သိမ်းထားသည်များ သွင်းမည်။</string>
|
||||
<string name="something_went_wrong">တစ်စုံတစ်ခုမှားယွင်းနေသည်။ထပ်မံကြိုးစားပါ။</string>
|
||||
<string name="image_format_not_supported">ဤရုပ်ပုံဖိုင်အမျိုးအစားအား လက်မခံပါ။</string>
|
||||
<string name="install_an_email">တုံ့ပြန်ချက်ပေးပို့ရန် အီးမေးလ်အက်ပ် တစ်ခုအရင်သွင်းပါ</string>
|
||||
|
|
|
@ -110,7 +110,6 @@
|
|||
<string name="choose_another_folder">Velg en annen mappe</string>
|
||||
<string name="cant_find_folder">Finner ikke mappen. Den kan ha blitt flyttet eller slettet</string>
|
||||
<string name="invalid_backup">Ugyldig sikkerhetskopi</string>
|
||||
<string name="imported_backup">Importert sikkerhetskopi</string>
|
||||
<string name="exporting_backup">Eksporterer sikkerhetskopi</string>
|
||||
<string name="importing_backup">Importerer sikkerhetskopi</string>
|
||||
<string name="calculating">Beregner…</string>
|
||||
|
|
|
@ -165,7 +165,6 @@
|
|||
<string name="choose_another_folder">Kies een andere map</string>
|
||||
<string name="cant_find_folder">Kan map niet vinden. Deze is mogelijk verplaatst of verwijderd</string>
|
||||
<string name="invalid_backup">Ongeldige back-up</string>
|
||||
<string name="imported_backup">Geïmporteerde back-up</string>
|
||||
<string name="exporting_backup">Back-up exporteren</string>
|
||||
<string name="importing_backup">Back-up importeren</string>
|
||||
<string name="calculating">Berekenen…</string>
|
||||
|
|
|
@ -110,7 +110,6 @@
|
|||
<string name="choose_another_folder">Vel ei anna mappe</string>
|
||||
<string name="cant_find_folder">Finn ikkje mappa. Den kan ha blitt flytta eller sletta</string>
|
||||
<string name="invalid_backup">Ugyldeg sikkerheitskopi</string>
|
||||
<string name="imported_backup">Importert sikkerheitskopi</string>
|
||||
<string name="exporting_backup">Eksporterer sikkerheitskopi</string>
|
||||
<string name="importing_backup">Importerer sikkerheitskopi</string>
|
||||
<string name="calculating">Bereknar…</string>
|
||||
|
|
|
@ -112,7 +112,6 @@
|
|||
<string name="choose_another_folder">Wybierz inny katalog</string>
|
||||
<string name="cant_find_folder">Nie można znaleźć katalogu. Być może został przeniesiony lub usunięty</string>
|
||||
<string name="invalid_backup">Niewłaściwy plik kopii zapasowej</string>
|
||||
<string name="imported_backup">Kopia zapasowa została przywrócona</string>
|
||||
<string name="exporting_backup">Wykonywanie kopii zapasowej</string>
|
||||
<string name="importing_backup">Przywracanie kopii zapasowej</string>
|
||||
<string name="calculating">Obliczanie…</string>
|
||||
|
|
|
@ -72,7 +72,6 @@
|
|||
<string name="cant_open_link">Nu se poate deschide link-ul</string>
|
||||
<string name="invalid_image">Imagine invalidă</string>
|
||||
<string name="invalid_backup">Backup invalid</string>
|
||||
<string name="imported_backup">Backup importat</string>
|
||||
<string name="something_went_wrong">Ceva a mers nu cum trebuie. Vă rog să încercați din nou.</string>
|
||||
<string name="image_format_not_supported">Formatul imaginii nu este acceptat</string>
|
||||
<string name="install_an_email">Instalați o aplicație de e-mail pentru a trimite feedback</string>
|
||||
|
|
|
@ -113,7 +113,6 @@
|
|||
<string name="choose_another_folder">Izberite drugo mapo</string>
|
||||
<string name="cant_find_folder">Mapa ne obstaja. Morda je bila premaknjena ali izbrisana.</string>
|
||||
<string name="invalid_backup">Neveljavna varnostna kopija</string>
|
||||
<string name="imported_backup">Varnostna kopija je bila uvožena</string>
|
||||
<string name="exporting_backup">Izvažanje varnostne kopije</string>
|
||||
<string name="importing_backup">Uvažanje varnostne kopije</string>
|
||||
<string name="calculating">Izračun…</string>
|
||||
|
|
|
@ -111,7 +111,6 @@
|
|||
<string name="choose_another_folder">Chọn thư mục khác</string>
|
||||
<string name="cant_find_folder">Không tìm thấy thư mục. Có lẽ nó đã bị đổi tên hoặc xóa</string>
|
||||
<string name="invalid_backup">Bản sao lưu không hợp lệ</string>
|
||||
<string name="imported_backup">Đã nhập bản sao lưu</string>
|
||||
<string name="exporting_backup">Đã xuất bản sao lưu</string>
|
||||
<string name="calculating">Đang xử lý…</string>
|
||||
|
||||
|
|
|
@ -116,7 +116,6 @@
|
|||
<string name="choose_another_folder">选择另一个文件夹</string>
|
||||
<string name="cant_find_folder">找不到该文件夹。可能被移动或删除了。</string>
|
||||
<string name="invalid_backup">无效的备份</string>
|
||||
<string name="imported_backup">已导入备份</string>
|
||||
|
||||
<!-- Widget -->
|
||||
<string name="single_note_or_list">单条笔记</string>
|
||||
|
|
|
@ -186,9 +186,15 @@
|
|||
<string name="help">Help</string>
|
||||
<string name="google_keep_help">In order to import your Notes from Google Keep you must download your Google Takeout ZIP file. Click Help to get more information.\n\nIf you already have a Takeout ZIP file, click Import and choose the ZIP file.</string>
|
||||
<string name="evernote_help">In order to import your Notes from Evernote you must export your Evernote Notebook as ENEX. Click Help to get more information.\n\nIf you already have a ENEX file, click Import and choose it.</string>
|
||||
<string name="imported_backup">Imported backup</string>
|
||||
<plurals name="imported_notes">
|
||||
<item quantity="one">Imported %s Note</item>
|
||||
<item quantity="other">Imported %s Notes</item>
|
||||
</plurals>
|
||||
<string name="exporting_backup">Exporting backup</string>
|
||||
<string name="importing_backup">Importing backup</string>
|
||||
<string name="extracted_files">Files extracted</string>
|
||||
<string name="imported_files">Files imported</string>
|
||||
<string name="imported_notes">Notes imported</string>
|
||||
<string name="calculating">Calculating…</string>
|
||||
<string name="clear_data_message">All Notes, Images, Files, Audios will be permanently deleted</string>
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class NotesImporterTest {
|
|||
|
||||
private fun testImport(importSource: ImportSource, expectedAmountNotes: Int) {
|
||||
val importSourceFile = prepareImportSources(importSource)
|
||||
val importOutputFolder = prepareMediaFolder(importSource)
|
||||
val importOutputFolder = prepareMediaFolder()
|
||||
runBlocking {
|
||||
NotesImporter(application, database).import(importSourceFile.toUri(), importSource)
|
||||
|
||||
|
@ -108,12 +108,13 @@ class NotesImporterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun prepareMediaFolder(importSource: ImportSource): String {
|
||||
private fun prepareMediaFolder(): String {
|
||||
val dir =
|
||||
File.createTempFile("notallyxNotesImporterTest", importSource.folderName).apply {
|
||||
delete()
|
||||
mkdirs()
|
||||
}
|
||||
File.createTempFile("notallyxNotesImporterTest", NotesImporter.IMPORT_CACHE_FOLDER)
|
||||
.apply {
|
||||
delete()
|
||||
mkdirs()
|
||||
}
|
||||
val path = dir.toPath().toString()
|
||||
ShadowEnvironment.addExternalDir(path)
|
||||
println("Output folder: $path")
|
||||
|
@ -121,8 +122,8 @@ class NotesImporterTest {
|
|||
}
|
||||
|
||||
private fun prepareImportSources(importSource: ImportSource): File {
|
||||
val tempDir = Files.createTempDirectory("imports-${importSource.folderName}").toFile()
|
||||
copyTestFilesToTempDir("imports/${importSource.folderName}", tempDir)
|
||||
val tempDir = Files.createTempDirectory("imports-${importSource.name.lowercase()}").toFile()
|
||||
copyTestFilesToTempDir("imports/${importSource.name.lowercase()}", tempDir)
|
||||
println("Input folder: ${tempDir.absolutePath}")
|
||||
return when (importSource) {
|
||||
ImportSource.GOOGLE_KEEP -> File(tempDir, "Takeout.zip")
|
||||
|
|
|
@ -49,7 +49,7 @@ class GoogleKeepImporterTest {
|
|||
labels = listOf("Label1", "Label2"),
|
||||
body = "This is some note, nothing special",
|
||||
)
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class GoogleKeepImporterTest {
|
|||
"""
|
||||
.trimIndent()
|
||||
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertThat(actual)
|
||||
.extracting("title", "folder")
|
||||
|
@ -85,7 +85,7 @@ class GoogleKeepImporterTest {
|
|||
"""
|
||||
.trimIndent()
|
||||
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertThat(actual)
|
||||
.extracting("title", "folder")
|
||||
|
@ -103,7 +103,7 @@ class GoogleKeepImporterTest {
|
|||
"""
|
||||
.trimIndent()
|
||||
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertThat(actual).extracting("title", "pinned").containsExactly("Pinned Note", true)
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ class GoogleKeepImporterTest {
|
|||
"""
|
||||
.trimIndent()
|
||||
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertThat(actual)
|
||||
.extracting("type", "title", "items")
|
||||
|
@ -158,7 +158,7 @@ class GoogleKeepImporterTest {
|
|||
"""
|
||||
.trimIndent()
|
||||
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertThat(actual)
|
||||
.extracting("title", "images")
|
||||
|
@ -184,7 +184,7 @@ class GoogleKeepImporterTest {
|
|||
"""
|
||||
.trimIndent()
|
||||
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertThat(actual)
|
||||
.extracting("title", "files")
|
||||
|
@ -210,7 +210,7 @@ class GoogleKeepImporterTest {
|
|||
"""
|
||||
.trimIndent()
|
||||
|
||||
val actual = importer.parseToBaseNote(json)
|
||||
val actual = with(importer) { json.parseToBaseNote() }
|
||||
|
||||
assertThat(actual.title).isEqualTo("Audio Note")
|
||||
assertThat(actual.audios[0].name).isEqualTo("audio.3gp")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue