mirror of
https://github.com/PhilKes/NotallyX.git
synced 2025-06-28 20:29:54 +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 com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask
|
||||||
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
@ -120,8 +119,11 @@ dependencies {
|
||||||
implementation "com.github.bumptech.glide:glide:4.15.1"
|
implementation "com.github.bumptech.glide:glide:4.15.1"
|
||||||
implementation "com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0"
|
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.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'
|
implementation 'org.jsoup:jsoup:1.18.1'
|
||||||
|
|
||||||
testImplementation "junit:junit:4.13.2"
|
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 ** extends androidx.navigation.Navigator
|
||||||
-keep class ** implements org.ocpsoft.prettytime.TimeUnit
|
-keep class ** implements org.ocpsoft.prettytime.TimeUnit
|
||||||
|
|
||||||
|
# SQLCipher
|
||||||
-keep class net.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.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.philkes.notallyx.data.model.BaseNote
|
import com.philkes.notallyx.data.model.BaseNote
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface ExternalImporter {
|
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
|
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.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.philkes.notallyx.R
|
import com.philkes.notallyx.R
|
||||||
import com.philkes.notallyx.data.DataUtil
|
import com.philkes.notallyx.data.DataUtil
|
||||||
import com.philkes.notallyx.data.NotallyDatabase
|
import com.philkes.notallyx.data.NotallyDatabase
|
||||||
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter
|
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter
|
||||||
import com.philkes.notallyx.data.imports.google.GoogleKeepImporter
|
import com.philkes.notallyx.data.imports.google.GoogleKeepImporter
|
||||||
import com.philkes.notallyx.data.model.Audio
|
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.FileAttachment
|
||||||
import com.philkes.notallyx.data.model.Label
|
import com.philkes.notallyx.data.model.Label
|
||||||
import com.philkes.notallyx.presentation.viewmodel.NotallyModel
|
import com.philkes.notallyx.presentation.viewmodel.NotallyModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class NotesImporter(private val app: Application, private val database: NotallyDatabase) {
|
class NotesImporter(private val app: Application, private val database: NotallyDatabase) {
|
||||||
|
|
||||||
suspend fun import(uri: Uri, importSource: ImportSource) {
|
suspend fun import(
|
||||||
val (notes, importDataFolder) =
|
uri: Uri,
|
||||||
when (importSource) {
|
importSource: ImportSource,
|
||||||
ImportSource.GOOGLE_KEEP -> GoogleKeepImporter().importFrom(uri, app)
|
progress: MutableLiveData<ImportProgress>? = null,
|
||||||
ImportSource.EVERNOTE -> EvernoteImporter().importFrom(uri, app)
|
): Int {
|
||||||
}
|
val tempDir = File(app.cacheDir, IMPORT_CACHE_FOLDER)
|
||||||
database.getLabelDao().insert(notes.flatMap { it.labels }.distinct().map { Label(it) })
|
if (!tempDir.exists()) {
|
||||||
importFiles(
|
tempDir.mkdirs()
|
||||||
notes.flatMap { it.files }.distinct(),
|
}
|
||||||
importDataFolder,
|
try {
|
||||||
NotallyModel.FileType.ANY,
|
val (notes, importDataFolder) =
|
||||||
)
|
try {
|
||||||
importFiles(
|
when (importSource) {
|
||||||
notes.flatMap { it.images }.distinct(),
|
ImportSource.GOOGLE_KEEP -> GoogleKeepImporter()
|
||||||
importDataFolder,
|
ImportSource.EVERNOTE -> EvernoteImporter()
|
||||||
NotallyModel.FileType.IMAGE,
|
}.import(app, uri, tempDir, progress)
|
||||||
)
|
} catch (e: Exception) {
|
||||||
importAudios(notes.flatMap { it.audios }.distinct(), importDataFolder)
|
Log.e(TAG, "import: failed", e)
|
||||||
database.getBaseNoteDao().insert(notes)
|
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(
|
private suspend fun importFiles(
|
||||||
files: List<FileAttachment>,
|
files: List<FileAttachment>,
|
||||||
sourceFolder: File,
|
sourceFolder: File,
|
||||||
fileType: NotallyModel.FileType,
|
fileType: NotallyModel.FileType,
|
||||||
|
progress: MutableLiveData<ImportProgress>?,
|
||||||
|
total: Int?,
|
||||||
|
counter: AtomicInteger?,
|
||||||
) {
|
) {
|
||||||
files.forEach { file ->
|
files.forEach { file ->
|
||||||
val uri = File(sourceFolder, file.localName).toUri()
|
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.originalName = fileAttachment.originalName
|
||||||
file.mimeType = fileAttachment.mimeType
|
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 ->
|
audios.forEach { originalAudio ->
|
||||||
val file = File(sourceFolder, originalAudio.name)
|
val file = File(sourceFolder, originalAudio.name)
|
||||||
val audio = DataUtil.addAudio(app, file, false)
|
val audio = DataUtil.addAudio(app, file, false)
|
||||||
originalAudio.name = audio.name
|
originalAudio.name = audio.name
|
||||||
originalAudio.duration = if (audio.duration == 0L) null else audio.duration
|
originalAudio.duration = if (audio.duration == 0L) null else audio.duration
|
||||||
originalAudio.timestamp = audio.timestamp
|
originalAudio.timestamp = audio.timestamp
|
||||||
|
progress?.postValue(
|
||||||
|
ImportProgress(
|
||||||
|
current = counter.getAndIncrement(),
|
||||||
|
total = totalFiles,
|
||||||
|
stage = ImportStage.IMPORT_FILES,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NotesImporter"
|
private const val TAG = "NotesImporter"
|
||||||
|
const val IMPORT_CACHE_FOLDER = "imports"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ImportSource(
|
enum class ImportSource(
|
||||||
val folderName: String,
|
|
||||||
val displayNameResId: Int,
|
val displayNameResId: Int,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val helpTextResId: Int,
|
val helpTextResId: Int,
|
||||||
|
@ -83,7 +141,6 @@ enum class ImportSource(
|
||||||
val iconResId: Int,
|
val iconResId: Int,
|
||||||
) {
|
) {
|
||||||
GOOGLE_KEEP(
|
GOOGLE_KEEP(
|
||||||
"googlekeep",
|
|
||||||
R.string.google_keep,
|
R.string.google_keep,
|
||||||
"application/zip",
|
"application/zip",
|
||||||
R.string.google_keep_help,
|
R.string.google_keep_help,
|
||||||
|
@ -91,7 +148,6 @@ enum class ImportSource(
|
||||||
R.drawable.icon_google_keep,
|
R.drawable.icon_google_keep,
|
||||||
),
|
),
|
||||||
EVERNOTE(
|
EVERNOTE(
|
||||||
"evernote",
|
|
||||||
R.string.evernote,
|
R.string.evernote,
|
||||||
"*/*", // 'application/enex+xml' is not recognized
|
"*/*", // 'application/enex+xml' is not recognized
|
||||||
R.string.evernote_help,
|
R.string.evernote_help,
|
||||||
|
@ -99,11 +155,3 @@ enum class ImportSource(
|
||||||
R.drawable.icon_evernote,
|
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.net.Uri
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.philkes.notallyx.R
|
import com.philkes.notallyx.R
|
||||||
import com.philkes.notallyx.data.imports.ExternalImporter
|
import com.philkes.notallyx.data.imports.ExternalImporter
|
||||||
import com.philkes.notallyx.data.imports.ImportException
|
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.evernote.EvernoteImporter.Companion.parseTimestamp
|
||||||
import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml
|
import com.philkes.notallyx.data.imports.parseBodyAndSpansFromHtml
|
||||||
import com.philkes.notallyx.data.model.Audio
|
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.ListItem
|
||||||
import com.philkes.notallyx.data.model.Type
|
import com.philkes.notallyx.data.model.Type
|
||||||
import com.philkes.notallyx.utils.IO.write
|
import com.philkes.notallyx.utils.IO.write
|
||||||
|
import com.philkes.notallyx.utils.Operations
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -31,25 +34,38 @@ import org.simpleframework.xml.core.Persister
|
||||||
class EvernoteImporter : ExternalImporter {
|
class EvernoteImporter : ExternalImporter {
|
||||||
private val serializer: Serializer = Persister(AnnotationStrategy())
|
private val serializer: Serializer = Persister(AnnotationStrategy())
|
||||||
|
|
||||||
override fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File> {
|
override fun import(
|
||||||
if (MimeTypeMap.getFileExtensionFromUrl(uri.toString()) != "enex") {
|
app: Application,
|
||||||
throw ImportException(R.string.invalid_evernote)
|
source: Uri,
|
||||||
}
|
destination: File,
|
||||||
val tempDir = File(app.cacheDir, ImportSource.EVERNOTE.folderName)
|
progress: MutableLiveData<ImportProgress>?,
|
||||||
if (!tempDir.exists()) {
|
): Pair<List<BaseNote>, File> {
|
||||||
tempDir.mkdirs()
|
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 =
|
val evernoteExport: EvernoteExport =
|
||||||
parseExport(app.contentResolver.openInputStream(uri)!!)!!
|
parseExport(app.contentResolver.openInputStream(source)!!)!!
|
||||||
saveResourcesToFiles(
|
|
||||||
evernoteExport.notes.flatMap { it.resources }.distinctBy { it.attributes?.fileName },
|
val total = evernoteExport.notes.size
|
||||||
tempDir,
|
progress?.postValue(ImportProgress(total = total))
|
||||||
)
|
var counter = 1
|
||||||
try {
|
try {
|
||||||
val notes = evernoteExport.notes.map { it.mapToBaseNote() }
|
val notes =
|
||||||
return Pair(notes, tempDir)
|
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) {
|
} 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)
|
throw ImportException(R.string.invalid_evernote, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveResourcesToFiles(resources: Collection<EvernoteResource>, dir: File) {
|
private fun saveResourcesToFiles(
|
||||||
resources.forEach {
|
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 file = File(dir, it.attributes!!.fileName)
|
||||||
val data = Base64.decode(it.data!!.content.trimStart(), Base64.DEFAULT)
|
try {
|
||||||
file.write(data)
|
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.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.philkes.notallyx.R
|
import com.philkes.notallyx.R
|
||||||
import com.philkes.notallyx.data.imports.ExternalImporter
|
import com.philkes.notallyx.data.imports.ExternalImporter
|
||||||
import com.philkes.notallyx.data.imports.ImportException
|
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.imports.parseBodyAndSpansFromHtml
|
||||||
import com.philkes.notallyx.data.model.Audio
|
import com.philkes.notallyx.data.model.Audio
|
||||||
import com.philkes.notallyx.data.model.BaseNote
|
import com.philkes.notallyx.data.model.BaseNote
|
||||||
|
@ -32,58 +34,72 @@ class GoogleKeepImporter : ExternalImporter {
|
||||||
allowTrailingComma = true
|
allowTrailingComma = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun importFrom(uri: Uri, app: Application): Pair<List<BaseNote>, File> {
|
override fun import(
|
||||||
val tempDir = File(app.cacheDir, ImportSource.GOOGLE_KEEP.folderName)
|
app: Application,
|
||||||
if (!tempDir.exists()) {
|
source: Uri,
|
||||||
tempDir.mkdirs()
|
destination: File,
|
||||||
}
|
progress: MutableLiveData<ImportProgress>?,
|
||||||
|
): Pair<List<BaseNote>, File> {
|
||||||
|
progress?.postValue(ImportProgress(indeterminate = true, stage = ImportStage.EXTRACT_FILES))
|
||||||
val dataFolder =
|
val dataFolder =
|
||||||
try {
|
try {
|
||||||
unzip(tempDir, app.contentResolver.openInputStream(uri)!!)
|
unzip(destination, app.contentResolver.openInputStream(source)!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw ImportException(R.string.invalid_google_keep, e)
|
throw ImportException(R.string.invalid_google_keep, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dataFolder.exists()) {
|
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
|
dataFolder
|
||||||
.walk()
|
.listFiles { file ->
|
||||||
.mapNotNull { importFile ->
|
file.isFile && file.extension.equals("json", ignoreCase = true)
|
||||||
if (importFile.extension == "json") {
|
}
|
||||||
parseToBaseNote(importFile.readText())
|
?.toList() ?: emptyList()
|
||||||
} else null
|
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()
|
.toList()
|
||||||
return Pair(baseNotes, dataFolder)
|
return Pair(baseNotes, dataFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseToBaseNote(jsonString: String): BaseNote {
|
fun String.parseToBaseNote(): BaseNote {
|
||||||
val keepNote = json.decodeFromString<KeepNote>(jsonString)
|
val googleKeepNote = json.decodeFromString<GoogleKeepNote>(this)
|
||||||
|
|
||||||
val (body, spans) =
|
val (body, spans) =
|
||||||
parseBodyAndSpansFromHtml(
|
parseBodyAndSpansFromHtml(
|
||||||
keepNote.textContentHtml,
|
googleKeepNote.textContentHtml,
|
||||||
paragraphsAsNewLine = true,
|
paragraphsAsNewLine = true,
|
||||||
brTagsAsNewLine = true,
|
brTagsAsNewLine = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
val images =
|
val images =
|
||||||
keepNote.attachments
|
googleKeepNote.attachments
|
||||||
.filter { it.mimetype.startsWith("image") }
|
.filter { it.mimetype.startsWith("image") }
|
||||||
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
|
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
|
||||||
val files =
|
val files =
|
||||||
keepNote.attachments
|
googleKeepNote.attachments
|
||||||
.filter { !it.mimetype.startsWith("audio") && !it.mimetype.startsWith("image") }
|
.filter { !it.mimetype.startsWith("audio") && !it.mimetype.startsWith("image") }
|
||||||
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
|
.map { FileAttachment(it.filePath, it.filePath, it.mimetype) }
|
||||||
val audios =
|
val audios =
|
||||||
keepNote.attachments
|
googleKeepNote.attachments
|
||||||
.filter { it.mimetype.startsWith("audio") }
|
.filter { it.mimetype.startsWith("audio") }
|
||||||
.map { Audio(it.filePath, 0L, System.currentTimeMillis()) }
|
.map { Audio(it.filePath, 0L, System.currentTimeMillis()) }
|
||||||
val items =
|
val items =
|
||||||
keepNote.listContent.mapIndexed { index, item ->
|
googleKeepNote.listContent.mapIndexed { index, item ->
|
||||||
ListItem(
|
ListItem(
|
||||||
body = item.text,
|
body = item.text,
|
||||||
checked = item.isChecked,
|
checked = item.isChecked,
|
||||||
|
@ -95,19 +111,19 @@ class GoogleKeepImporter : ExternalImporter {
|
||||||
|
|
||||||
return BaseNote(
|
return BaseNote(
|
||||||
id = 0L, // Auto-generated
|
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 =
|
folder =
|
||||||
when {
|
when {
|
||||||
keepNote.isTrashed -> Folder.DELETED
|
googleKeepNote.isTrashed -> Folder.DELETED
|
||||||
keepNote.isArchived -> Folder.ARCHIVED
|
googleKeepNote.isArchived -> Folder.ARCHIVED
|
||||||
else -> Folder.NOTES
|
else -> Folder.NOTES
|
||||||
},
|
},
|
||||||
color = Color.DEFAULT, // Ignoring color mapping
|
color = Color.DEFAULT, // Ignoring color mapping
|
||||||
title = keepNote.title,
|
title = googleKeepNote.title,
|
||||||
pinned = keepNote.isPinned,
|
pinned = googleKeepNote.isPinned,
|
||||||
timestamp = keepNote.createdTimestampUsec / 1000,
|
timestamp = googleKeepNote.createdTimestampUsec / 1000,
|
||||||
modifiedTimestamp = keepNote.userEditedTimestampUsec / 1000,
|
modifiedTimestamp = googleKeepNote.userEditedTimestampUsec / 1000,
|
||||||
labels = keepNote.labels.map { it.name },
|
labels = googleKeepNote.labels.map { it.name },
|
||||||
body = body,
|
body = body,
|
||||||
spans = spans,
|
spans = spans,
|
||||||
items = items,
|
items = items,
|
||||||
|
|
|
@ -4,8 +4,8 @@ import com.philkes.notallyx.data.model.Color
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class KeepNote(
|
data class GoogleKeepNote(
|
||||||
val attachments: List<KeepAttachment> = listOf(),
|
val attachments: List<GoogleKeepAttachment> = listOf(),
|
||||||
val color: String = Color.DEFAULT.name,
|
val color: String = Color.DEFAULT.name,
|
||||||
val isTrashed: Boolean = false,
|
val isTrashed: Boolean = false,
|
||||||
val isArchived: Boolean = false,
|
val isArchived: Boolean = false,
|
||||||
|
@ -13,14 +13,14 @@ data class KeepNote(
|
||||||
val textContent: String = "",
|
val textContent: String = "",
|
||||||
val textContentHtml: String = "",
|
val textContentHtml: String = "",
|
||||||
val title: String = "",
|
val title: String = "",
|
||||||
val labels: List<KeepLabel> = listOf(),
|
val labels: List<GoogleKeepLabel> = listOf(),
|
||||||
val userEditedTimestampUsec: Long = System.currentTimeMillis(),
|
val userEditedTimestampUsec: Long = System.currentTimeMillis(),
|
||||||
val createdTimestampUsec: 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 androidx.lifecycle.Observer
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.philkes.notallyx.R
|
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.Folder
|
||||||
import com.philkes.notallyx.data.model.SpanRepresentation
|
import com.philkes.notallyx.data.model.SpanRepresentation
|
||||||
import com.philkes.notallyx.data.model.getUrl
|
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 {
|
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 {
|
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,
|
context: Context,
|
||||||
layoutInflater: LayoutInflater,
|
layoutInflater: LayoutInflater,
|
||||||
viewLifecycleOwner: LifecycleOwner,
|
viewLifecycleOwner: LifecycleOwner,
|
||||||
titleId: Int,
|
titleId: Int,
|
||||||
|
renderProgress: ((context: Context, binding: DialogProgressBinding, progress: T) -> Unit)? =
|
||||||
|
null,
|
||||||
) {
|
) {
|
||||||
val dialogBinding = DialogProgressBinding.inflate(layoutInflater)
|
val dialogBinding = DialogProgressBinding.inflate(layoutInflater)
|
||||||
val dialog =
|
val dialog =
|
||||||
|
@ -398,7 +422,10 @@ private fun MutableLiveData<out Progress>.setupProgressDialog(
|
||||||
max = progress.total
|
max = progress.total
|
||||||
setProgressCompat(progress.current, true)
|
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()
|
dialog.show()
|
||||||
|
@ -455,3 +482,7 @@ fun MaterialAlertDialogBuilder.showAndFocus(view: View): AlertDialog {
|
||||||
}
|
}
|
||||||
return dialog
|
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.activity.note.EditNoteActivity
|
||||||
import com.philkes.notallyx.presentation.add
|
import com.philkes.notallyx.presentation.add
|
||||||
import com.philkes.notallyx.presentation.applySpans
|
import com.philkes.notallyx.presentation.applySpans
|
||||||
|
import com.philkes.notallyx.presentation.getQuantityString
|
||||||
import com.philkes.notallyx.presentation.movedToResId
|
import com.philkes.notallyx.presentation.movedToResId
|
||||||
import com.philkes.notallyx.presentation.view.main.ColorAdapter
|
import com.philkes.notallyx.presentation.view.main.ColorAdapter
|
||||||
import com.philkes.notallyx.presentation.view.misc.MenuDialog
|
import com.philkes.notallyx.presentation.view.misc.MenuDialog
|
||||||
|
@ -217,7 +218,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
||||||
val ids = model.moveBaseNotes(folderTo)
|
val ids = model.moveBaseNotes(folderTo)
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
findViewById(R.id.DrawerLayout),
|
findViewById(R.id.DrawerLayout),
|
||||||
resources.getQuantityString(folderTo.movedToResId(), ids.size, ids.size),
|
getQuantityString(folderTo.movedToResId(), ids.size),
|
||||||
Snackbar.LENGTH_SHORT,
|
Snackbar.LENGTH_SHORT,
|
||||||
)
|
)
|
||||||
.apply { setAction(R.string.undo) { model.moveBaseNotes(ids, folderFrom) } }
|
.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.EditActivity.Companion.NOTE_ID
|
||||||
import com.philkes.notallyx.presentation.activity.note.EditListActivity
|
import com.philkes.notallyx.presentation.activity.note.EditListActivity
|
||||||
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
|
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.movedToResId
|
||||||
import com.philkes.notallyx.presentation.view.Constants
|
import com.philkes.notallyx.presentation.view.Constants
|
||||||
import com.philkes.notallyx.presentation.view.main.BaseNoteAdapter
|
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)!!)
|
val folderTo = Folder.valueOf(data.getStringExtra(FOLDER_TO)!!)
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
binding!!.root,
|
binding!!.root,
|
||||||
resources.getQuantityString(folderTo.movedToResId(), 1, 1),
|
requireContext().getQuantityString(folderTo.movedToResId(), 1),
|
||||||
Snackbar.LENGTH_SHORT,
|
Snackbar.LENGTH_SHORT,
|
||||||
)
|
)
|
||||||
.apply {
|
.apply {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import com.philkes.notallyx.databinding.PreferenceSeekbarBinding
|
||||||
import com.philkes.notallyx.databinding.TextInputDialogBinding
|
import com.philkes.notallyx.databinding.TextInputDialogBinding
|
||||||
import com.philkes.notallyx.presentation.canAuthenticateWithBiometrics
|
import com.philkes.notallyx.presentation.canAuthenticateWithBiometrics
|
||||||
import com.philkes.notallyx.presentation.checkedTag
|
import com.philkes.notallyx.presentation.checkedTag
|
||||||
|
import com.philkes.notallyx.presentation.setupImportProgressDialog
|
||||||
import com.philkes.notallyx.presentation.setupProgressDialog
|
import com.philkes.notallyx.presentation.setupProgressDialog
|
||||||
import com.philkes.notallyx.presentation.view.misc.AutoBackup
|
import com.philkes.notallyx.presentation.view.misc.AutoBackup
|
||||||
import com.philkes.notallyx.presentation.view.misc.AutoBackupMax
|
import com.philkes.notallyx.presentation.view.misc.AutoBackupMax
|
||||||
|
@ -124,7 +125,7 @@ class SettingsFragment : Fragment() {
|
||||||
binding.ClearData.setOnClickListener { clearData() }
|
binding.ClearData.setOnClickListener { clearData() }
|
||||||
|
|
||||||
model.exportProgress.setupProgressDialog(this, R.string.exporting_backup)
|
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)
|
model.deletionProgress.setupProgressDialog(this, R.string.deleting_files)
|
||||||
|
|
||||||
binding.GitHub.setOnClickListener { openLink("https://github.com/PhilKes/NotallyX") }
|
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.activity.LockedActivity
|
||||||
import com.philkes.notallyx.presentation.add
|
import com.philkes.notallyx.presentation.add
|
||||||
import com.philkes.notallyx.presentation.displayFormattedTimestamp
|
import com.philkes.notallyx.presentation.displayFormattedTimestamp
|
||||||
|
import com.philkes.notallyx.presentation.getQuantityString
|
||||||
import com.philkes.notallyx.presentation.setupProgressDialog
|
import com.philkes.notallyx.presentation.setupProgressDialog
|
||||||
import com.philkes.notallyx.presentation.view.Constants
|
import com.philkes.notallyx.presentation.view.Constants
|
||||||
import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByCreationDate
|
import com.philkes.notallyx.presentation.view.misc.NotesSorting.autoSortByCreationDate
|
||||||
|
@ -518,7 +519,7 @@ abstract class EditActivity(private val type: Type) : LockedActivity<ActivityEdi
|
||||||
} else {
|
} else {
|
||||||
R.plurals.cant_add_files
|
R.plurals.cant_add_files
|
||||||
}
|
}
|
||||||
val title = resources.getQuantityString(message, errors.size, errors.size)
|
val title = getQuantityString(message, errors.size)
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setView(recyclerView)
|
.setView(recyclerView)
|
||||||
|
|
|
@ -15,7 +15,6 @@ import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.util.PatternsCompat
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.philkes.notallyx.R
|
import com.philkes.notallyx.R
|
||||||
import com.philkes.notallyx.data.model.Type
|
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.CommonDao
|
||||||
import com.philkes.notallyx.data.dao.LabelDao
|
import com.philkes.notallyx.data.dao.LabelDao
|
||||||
import com.philkes.notallyx.data.imports.ImportException
|
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.ImportSource
|
||||||
import com.philkes.notallyx.data.imports.NotesImporter
|
import com.philkes.notallyx.data.imports.NotesImporter
|
||||||
import com.philkes.notallyx.data.model.Attachment
|
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.SearchResult
|
||||||
import com.philkes.notallyx.data.model.Type
|
import com.philkes.notallyx.data.model.Type
|
||||||
import com.philkes.notallyx.presentation.applySpans
|
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.AutoBackup
|
||||||
import com.philkes.notallyx.presentation.view.misc.ListInfo
|
import com.philkes.notallyx.presentation.view.misc.ListInfo
|
||||||
import com.philkes.notallyx.presentation.view.misc.Progress
|
import com.philkes.notallyx.presentation.view.misc.Progress
|
||||||
|
@ -109,7 +111,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
||||||
val fileRoot = app.getExternalFilesDirectory()
|
val fileRoot = app.getExternalFilesDirectory()
|
||||||
private val audioRoot = app.getExternalAudioDirectory()
|
private val audioRoot = app.getExternalAudioDirectory()
|
||||||
|
|
||||||
val importProgress = MutableLiveData<Progress>()
|
val importProgress = MutableLiveData<ImportProgress>()
|
||||||
val exportProgress = MutableLiveData<Progress>()
|
val exportProgress = MutableLiveData<Progress>()
|
||||||
val deletionProgress = MutableLiveData<Progress>()
|
val deletionProgress = MutableLiveData<Progress>()
|
||||||
|
|
||||||
|
@ -219,8 +221,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importZipBackup(uri: Uri, password: String) {
|
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()
|
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) {
|
fun importXmlBackup(uri: Uri) {
|
||||||
|
@ -230,12 +239,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch(exceptionHandler) {
|
viewModelScope.launch(exceptionHandler) {
|
||||||
withContext(Dispatchers.IO) {
|
val importedNotes =
|
||||||
val stream = requireNotNull(app.contentResolver.openInputStream(uri))
|
withContext(Dispatchers.IO) {
|
||||||
val backup = XMLUtils.readBackupFromStream(stream)
|
val stream = requireNotNull(app.contentResolver.openInputStream(uri))
|
||||||
commonDao.importBackup(backup.first, backup.second)
|
val (baseNotes, labels) = XMLUtils.readBackupFromStream(stream)
|
||||||
}
|
commonDao.importBackup(baseNotes, labels)
|
||||||
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show()
|
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)
|
Operations.log(app, throwable)
|
||||||
}
|
}
|
||||||
viewModelScope.launch(exceptionHandler) {
|
viewModelScope.launch(exceptionHandler) {
|
||||||
withContext(Dispatchers.IO) { NotesImporter(app, database).import(uri, importSource) }
|
val importedNotes =
|
||||||
Toast.makeText(app, R.string.imported_backup, Toast.LENGTH_LONG).show()
|
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.NotallyDatabase
|
||||||
import com.philkes.notallyx.data.model.BaseNote
|
import com.philkes.notallyx.data.model.BaseNote
|
||||||
import com.philkes.notallyx.data.model.Type
|
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.view.misc.TextSize
|
||||||
import com.philkes.notallyx.presentation.widget.WidgetProvider.Companion.getSelectNoteIntent
|
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
|
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.graphics.BitmapFactory
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.philkes.notallyx.data.model.Attachment
|
import com.philkes.notallyx.data.model.Attachment
|
||||||
import com.philkes.notallyx.data.model.Audio
|
import com.philkes.notallyx.data.model.Audio
|
||||||
import com.philkes.notallyx.data.model.FileAttachment
|
import com.philkes.notallyx.data.model.FileAttachment
|
||||||
|
@ -85,7 +85,7 @@ object IO {
|
||||||
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||||
val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
|
val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
|
||||||
mimeType != null && hasAudio != null ||
|
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) {
|
} catch (e: Exception) {
|
||||||
false // An exception means it’s not a valid audio file
|
false // An exception means it’s not a valid audio file
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import androidx.core.database.getLongOrNull
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.philkes.notallyx.R
|
import com.philkes.notallyx.R
|
||||||
import com.philkes.notallyx.data.NotallyDatabase
|
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.BaseNote
|
||||||
import com.philkes.notallyx.data.model.Color
|
import com.philkes.notallyx.data.model.Color
|
||||||
import com.philkes.notallyx.data.model.Converters
|
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.Folder
|
||||||
import com.philkes.notallyx.data.model.Label
|
import com.philkes.notallyx.data.model.Label
|
||||||
import com.philkes.notallyx.data.model.Type
|
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_AUDIOS
|
||||||
import com.philkes.notallyx.utils.IO.SUBFOLDER_FILES
|
import com.philkes.notallyx.utils.IO.SUBFOLDER_FILES
|
||||||
import com.philkes.notallyx.utils.IO.SUBFOLDER_IMAGES
|
import com.philkes.notallyx.utils.IO.SUBFOLDER_IMAGES
|
||||||
|
@ -46,93 +48,115 @@ object Import {
|
||||||
zipFileUri: Uri,
|
zipFileUri: Uri,
|
||||||
databaseFolder: File,
|
databaseFolder: File,
|
||||||
zipPassword: String,
|
zipPassword: String,
|
||||||
importingBackup: MutableLiveData<Progress>? = null,
|
importingBackup: MutableLiveData<ImportProgress>? = null,
|
||||||
) {
|
) {
|
||||||
importingBackup?.postValue(Progress(indeterminate = true))
|
importingBackup?.postValue(ImportProgress(indeterminate = true))
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
val importedNotes =
|
||||||
val stream = requireNotNull(app.contentResolver.openInputStream(zipFileUri))
|
withContext(Dispatchers.IO) {
|
||||||
val tempZipFile = File(databaseFolder, "TEMP.zip")
|
val stream = requireNotNull(app.contentResolver.openInputStream(zipFileUri))
|
||||||
stream.copyToFile(tempZipFile)
|
val tempZipFile = File(databaseFolder, "TEMP.zip")
|
||||||
val zipFile = ZipFile(tempZipFile)
|
stream.copyToFile(tempZipFile)
|
||||||
if (zipFile.isEncrypted) {
|
val zipFile = ZipFile(tempZipFile)
|
||||||
zipFile.setPassword(zipPassword.toCharArray())
|
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
|
|
||||||
}
|
}
|
||||||
importingBackup?.postValue(Progress(0, total))
|
zipFile.extractFile(
|
||||||
|
NotallyDatabase.DatabaseName,
|
||||||
|
databaseFolder.path,
|
||||||
|
NotallyDatabase.DatabaseName,
|
||||||
|
)
|
||||||
|
|
||||||
val current = AtomicInteger(1)
|
val database =
|
||||||
val imageRoot = app.getExternalImagesDirectory()
|
SQLiteDatabase.openDatabase(
|
||||||
val fileRoot = app.getExternalFilesDirectory()
|
File(databaseFolder, NotallyDatabase.DatabaseName).path,
|
||||||
val audioRoot = app.getExternalAudioDirectory()
|
null,
|
||||||
baseNotes.forEach { baseNote ->
|
SQLiteDatabase.OPEN_READONLY,
|
||||||
importFiles(
|
)
|
||||||
app,
|
|
||||||
baseNote.images,
|
val labelCursor = database.query("Label", null, null, null, null, null, null)
|
||||||
SUBFOLDER_IMAGES,
|
val baseNoteCursor =
|
||||||
imageRoot,
|
database.query("BaseNote", null, null, null, null, null, null)
|
||||||
zipFile,
|
|
||||||
current,
|
val labels = labelCursor.toList { cursor -> cursor.toLabel() }
|
||||||
total,
|
|
||||||
|
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,
|
val current = AtomicInteger(1)
|
||||||
baseNote.files,
|
val imageRoot = app.getExternalImagesDirectory()
|
||||||
SUBFOLDER_FILES,
|
val fileRoot = app.getExternalFilesDirectory()
|
||||||
fileRoot,
|
val audioRoot = app.getExternalAudioDirectory()
|
||||||
zipFile,
|
baseNotes.forEach { baseNote ->
|
||||||
current,
|
importFiles(
|
||||||
total,
|
app,
|
||||||
)
|
baseNote.images,
|
||||||
baseNote.audios.forEach { audio ->
|
SUBFOLDER_IMAGES,
|
||||||
try {
|
imageRoot,
|
||||||
val audioFilePath = "$SUBFOLDER_AUDIOS/${audio.name}"
|
zipFile,
|
||||||
val entry = zipFile.getFileHeader(audioFilePath)
|
current,
|
||||||
if (entry != null) {
|
total,
|
||||||
val name = "${UUID.randomUUID()}.m4a"
|
importingBackup,
|
||||||
zipFile.extractFile(audioFilePath, audioRoot!!.path, name)
|
)
|
||||||
audio.name = name
|
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)
|
NotallyDatabase.getDatabase(app)
|
||||||
.value
|
.value
|
||||||
.getCommonDao()
|
.getCommonDao()
|
||||||
.importBackup(baseNotes, labels)
|
.importBackup(baseNotes, labels)
|
||||||
}
|
baseNotes.size
|
||||||
|
}
|
||||||
databaseFolder.clearDirectory()
|
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) {
|
} catch (e: ZipException) {
|
||||||
if (e.type == ZipException.Type.WRONG_PASSWORD) {
|
if (e.type == ZipException.Type.WRONG_PASSWORD) {
|
||||||
Toast.makeText(app, R.string.wrong_password, Toast.LENGTH_LONG).show()
|
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()
|
Toast.makeText(app, R.string.invalid_backup, Toast.LENGTH_LONG).show()
|
||||||
Operations.log(app, e)
|
Operations.log(app, e)
|
||||||
} finally {
|
} finally {
|
||||||
importingBackup?.value = Progress(inProgress = false)
|
importingBackup?.value = ImportProgress(inProgress = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +180,7 @@ object Import {
|
||||||
zipFile: ZipFile,
|
zipFile: ZipFile,
|
||||||
current: AtomicInteger,
|
current: AtomicInteger,
|
||||||
total: Int,
|
total: Int,
|
||||||
importingBackup: MutableLiveData<Progress>? = null,
|
importingBackup: MutableLiveData<ImportProgress>? = null,
|
||||||
) {
|
) {
|
||||||
files.forEach { file ->
|
files.forEach { file ->
|
||||||
try {
|
try {
|
||||||
|
@ -170,8 +194,13 @@ object Import {
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Operations.log(app, exception)
|
Operations.log(app, exception)
|
||||||
} finally {
|
} finally {
|
||||||
importingBackup?.postValue(Progress(current.get(), total))
|
importingBackup?.postValue(
|
||||||
current.getAndIncrement()
|
ImportProgress(
|
||||||
|
current.getAndIncrement(),
|
||||||
|
total,
|
||||||
|
stage = ImportStage.IMPORT_FILES,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,6 @@
|
||||||
<string name="choose_another_folder">Vybrat jinou složku</string>
|
<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="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="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="exporting_backup">Vytváří se záloha</string>
|
||||||
<string name="importing_backup">Importuje se záloha</string>
|
<string name="importing_backup">Importuje se záloha</string>
|
||||||
<string name="calculating">Probíhá výpočet…</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="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="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="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="exporting_backup">Backup Exportieren</string>
|
||||||
<string name="importing_backup">Backup Importieren</string>
|
<string name="importing_backup">Backup Importieren</string>
|
||||||
<string name="calculating">Berechne…</string>
|
<string name="calculating">Berechne…</string>
|
||||||
|
|
|
@ -68,7 +68,6 @@
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
<string name="invalid_backup">Copia de seguridad inválida</string>
|
<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_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>
|
<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="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="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="invalid_backup">Sauvegarde invalide</string>
|
||||||
<string name="imported_backup">Sauvegarde importée</string>
|
|
||||||
<string name="exporting_backup">Exportation de la sauvegarde</string>
|
<string name="exporting_backup">Exportation de la sauvegarde</string>
|
||||||
<string name="importing_backup">Importation de la sauvegarde</string>
|
<string name="importing_backup">Importation de la sauvegarde</string>
|
||||||
<string name="calculating">Calcul…</string>
|
<string name="calculating">Calcul…</string>
|
||||||
|
|
|
@ -67,7 +67,6 @@
|
||||||
<string name="empty_list">Daftar kosong</string>
|
<string name="empty_list">Daftar kosong</string>
|
||||||
<string name="cant_open_link">Tidak bisa membuka tautan</string>
|
<string name="cant_open_link">Tidak bisa membuka tautan</string>
|
||||||
<string name="invalid_backup">Cadangan tidak valid</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="something_went_wrong">Telah terjadi kesalahan. Silakan coba lagi.</string>
|
||||||
<string name="install_a_browser">Pasang peramban untuk membuka tautan ini</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="empty_list">Lista vuota</string>
|
||||||
<string name="cant_open_link">Impossibile aprire il link</string>
|
<string name="cant_open_link">Impossibile aprire il link</string>
|
||||||
<string name="invalid_backup">Backup invalido</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>
|
<string name="something_went_wrong">Qualcosa è andato storto. Per favore riprova.</string>
|
||||||
|
|
||||||
<!-- Settings Page -->
|
<!-- Settings Page -->
|
||||||
|
|
|
@ -71,7 +71,6 @@
|
||||||
<string name="cant_open_link">လင့်ကိုမဖွင့်နိုင်ပါ။</string>
|
<string name="cant_open_link">လင့်ကိုမဖွင့်နိုင်ပါ။</string>
|
||||||
<string name="invalid_image">ရုပ်ပုံမှားနေသည်။</string>
|
<string name="invalid_image">ရုပ်ပုံမှားနေသည်။</string>
|
||||||
<string name="invalid_backup">အရန်သိမ်းဖိုင်မှားနေသည်။</string>
|
<string name="invalid_backup">အရန်သိမ်းဖိုင်မှားနေသည်။</string>
|
||||||
<string name="imported_backup">အရန်သိမ်းထားသည်များ သွင်းမည်။</string>
|
|
||||||
<string name="something_went_wrong">တစ်စုံတစ်ခုမှားယွင်းနေသည်။ထပ်မံကြိုးစားပါ။</string>
|
<string name="something_went_wrong">တစ်စုံတစ်ခုမှားယွင်းနေသည်။ထပ်မံကြိုးစားပါ။</string>
|
||||||
<string name="image_format_not_supported">ဤရုပ်ပုံဖိုင်အမျိုးအစားအား လက်မခံပါ။</string>
|
<string name="image_format_not_supported">ဤရုပ်ပုံဖိုင်အမျိုးအစားအား လက်မခံပါ။</string>
|
||||||
<string name="install_an_email">တုံ့ပြန်ချက်ပေးပို့ရန် အီးမေးလ်အက်ပ် တစ်ခုအရင်သွင်းပါ</string>
|
<string name="install_an_email">တုံ့ပြန်ချက်ပေးပို့ရန် အီးမေးလ်အက်ပ် တစ်ခုအရင်သွင်းပါ</string>
|
||||||
|
|
|
@ -110,7 +110,6 @@
|
||||||
<string name="choose_another_folder">Velg en annen mappe</string>
|
<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="cant_find_folder">Finner ikke mappen. Den kan ha blitt flyttet eller slettet</string>
|
||||||
<string name="invalid_backup">Ugyldig sikkerhetskopi</string>
|
<string name="invalid_backup">Ugyldig sikkerhetskopi</string>
|
||||||
<string name="imported_backup">Importert sikkerhetskopi</string>
|
|
||||||
<string name="exporting_backup">Eksporterer sikkerhetskopi</string>
|
<string name="exporting_backup">Eksporterer sikkerhetskopi</string>
|
||||||
<string name="importing_backup">Importerer sikkerhetskopi</string>
|
<string name="importing_backup">Importerer sikkerhetskopi</string>
|
||||||
<string name="calculating">Beregner…</string>
|
<string name="calculating">Beregner…</string>
|
||||||
|
|
|
@ -165,7 +165,6 @@
|
||||||
<string name="choose_another_folder">Kies een andere map</string>
|
<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="cant_find_folder">Kan map niet vinden. Deze is mogelijk verplaatst of verwijderd</string>
|
||||||
<string name="invalid_backup">Ongeldige back-up</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="exporting_backup">Back-up exporteren</string>
|
||||||
<string name="importing_backup">Back-up importeren</string>
|
<string name="importing_backup">Back-up importeren</string>
|
||||||
<string name="calculating">Berekenen…</string>
|
<string name="calculating">Berekenen…</string>
|
||||||
|
|
|
@ -110,7 +110,6 @@
|
||||||
<string name="choose_another_folder">Vel ei anna mappe</string>
|
<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="cant_find_folder">Finn ikkje mappa. Den kan ha blitt flytta eller sletta</string>
|
||||||
<string name="invalid_backup">Ugyldeg sikkerheitskopi</string>
|
<string name="invalid_backup">Ugyldeg sikkerheitskopi</string>
|
||||||
<string name="imported_backup">Importert sikkerheitskopi</string>
|
|
||||||
<string name="exporting_backup">Eksporterer sikkerheitskopi</string>
|
<string name="exporting_backup">Eksporterer sikkerheitskopi</string>
|
||||||
<string name="importing_backup">Importerer sikkerheitskopi</string>
|
<string name="importing_backup">Importerer sikkerheitskopi</string>
|
||||||
<string name="calculating">Bereknar…</string>
|
<string name="calculating">Bereknar…</string>
|
||||||
|
|
|
@ -112,7 +112,6 @@
|
||||||
<string name="choose_another_folder">Wybierz inny katalog</string>
|
<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="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="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="exporting_backup">Wykonywanie kopii zapasowej</string>
|
||||||
<string name="importing_backup">Przywracanie kopii zapasowej</string>
|
<string name="importing_backup">Przywracanie kopii zapasowej</string>
|
||||||
<string name="calculating">Obliczanie…</string>
|
<string name="calculating">Obliczanie…</string>
|
||||||
|
|
|
@ -72,7 +72,6 @@
|
||||||
<string name="cant_open_link">Nu se poate deschide link-ul</string>
|
<string name="cant_open_link">Nu se poate deschide link-ul</string>
|
||||||
<string name="invalid_image">Imagine invalidă</string>
|
<string name="invalid_image">Imagine invalidă</string>
|
||||||
<string name="invalid_backup">Backup 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="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="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>
|
<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="choose_another_folder">Izberite drugo mapo</string>
|
||||||
<string name="cant_find_folder">Mapa ne obstaja. Morda je bila premaknjena ali izbrisana.</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="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="exporting_backup">Izvažanje varnostne kopije</string>
|
||||||
<string name="importing_backup">Uvažanje varnostne kopije</string>
|
<string name="importing_backup">Uvažanje varnostne kopije</string>
|
||||||
<string name="calculating">Izračun…</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="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="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="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="exporting_backup">Đã xuất bản sao lưu</string>
|
||||||
<string name="calculating">Đang xử lý…</string>
|
<string name="calculating">Đang xử lý…</string>
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,6 @@
|
||||||
<string name="choose_another_folder">选择另一个文件夹</string>
|
<string name="choose_another_folder">选择另一个文件夹</string>
|
||||||
<string name="cant_find_folder">找不到该文件夹。可能被移动或删除了。</string>
|
<string name="cant_find_folder">找不到该文件夹。可能被移动或删除了。</string>
|
||||||
<string name="invalid_backup">无效的备份</string>
|
<string name="invalid_backup">无效的备份</string>
|
||||||
<string name="imported_backup">已导入备份</string>
|
|
||||||
|
|
||||||
<!-- Widget -->
|
<!-- Widget -->
|
||||||
<string name="single_note_or_list">单条笔记</string>
|
<string name="single_note_or_list">单条笔记</string>
|
||||||
|
|
|
@ -186,9 +186,15 @@
|
||||||
<string name="help">Help</string>
|
<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="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="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="exporting_backup">Exporting backup</string>
|
||||||
<string name="importing_backup">Importing 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="calculating">Calculating…</string>
|
||||||
<string name="clear_data_message">All Notes, Images, Files, Audios will be permanently deleted</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) {
|
private fun testImport(importSource: ImportSource, expectedAmountNotes: Int) {
|
||||||
val importSourceFile = prepareImportSources(importSource)
|
val importSourceFile = prepareImportSources(importSource)
|
||||||
val importOutputFolder = prepareMediaFolder(importSource)
|
val importOutputFolder = prepareMediaFolder()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
NotesImporter(application, database).import(importSourceFile.toUri(), importSource)
|
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 =
|
val dir =
|
||||||
File.createTempFile("notallyxNotesImporterTest", importSource.folderName).apply {
|
File.createTempFile("notallyxNotesImporterTest", NotesImporter.IMPORT_CACHE_FOLDER)
|
||||||
delete()
|
.apply {
|
||||||
mkdirs()
|
delete()
|
||||||
}
|
mkdirs()
|
||||||
|
}
|
||||||
val path = dir.toPath().toString()
|
val path = dir.toPath().toString()
|
||||||
ShadowEnvironment.addExternalDir(path)
|
ShadowEnvironment.addExternalDir(path)
|
||||||
println("Output folder: $path")
|
println("Output folder: $path")
|
||||||
|
@ -121,8 +122,8 @@ class NotesImporterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun prepareImportSources(importSource: ImportSource): File {
|
private fun prepareImportSources(importSource: ImportSource): File {
|
||||||
val tempDir = Files.createTempDirectory("imports-${importSource.folderName}").toFile()
|
val tempDir = Files.createTempDirectory("imports-${importSource.name.lowercase()}").toFile()
|
||||||
copyTestFilesToTempDir("imports/${importSource.folderName}", tempDir)
|
copyTestFilesToTempDir("imports/${importSource.name.lowercase()}", tempDir)
|
||||||
println("Input folder: ${tempDir.absolutePath}")
|
println("Input folder: ${tempDir.absolutePath}")
|
||||||
return when (importSource) {
|
return when (importSource) {
|
||||||
ImportSource.GOOGLE_KEEP -> File(tempDir, "Takeout.zip")
|
ImportSource.GOOGLE_KEEP -> File(tempDir, "Takeout.zip")
|
||||||
|
|
|
@ -49,7 +49,7 @@ class GoogleKeepImporterTest {
|
||||||
labels = listOf("Label1", "Label2"),
|
labels = listOf("Label1", "Label2"),
|
||||||
body = "This is some note, nothing special",
|
body = "This is some note, nothing special",
|
||||||
)
|
)
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ class GoogleKeepImporterTest {
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertThat(actual)
|
assertThat(actual)
|
||||||
.extracting("title", "folder")
|
.extracting("title", "folder")
|
||||||
|
@ -85,7 +85,7 @@ class GoogleKeepImporterTest {
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertThat(actual)
|
assertThat(actual)
|
||||||
.extracting("title", "folder")
|
.extracting("title", "folder")
|
||||||
|
@ -103,7 +103,7 @@ class GoogleKeepImporterTest {
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertThat(actual).extracting("title", "pinned").containsExactly("Pinned Note", true)
|
assertThat(actual).extracting("title", "pinned").containsExactly("Pinned Note", true)
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ class GoogleKeepImporterTest {
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertThat(actual)
|
assertThat(actual)
|
||||||
.extracting("type", "title", "items")
|
.extracting("type", "title", "items")
|
||||||
|
@ -158,7 +158,7 @@ class GoogleKeepImporterTest {
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertThat(actual)
|
assertThat(actual)
|
||||||
.extracting("title", "images")
|
.extracting("title", "images")
|
||||||
|
@ -184,7 +184,7 @@ class GoogleKeepImporterTest {
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertThat(actual)
|
assertThat(actual)
|
||||||
.extracting("title", "files")
|
.extracting("title", "files")
|
||||||
|
@ -210,7 +210,7 @@ class GoogleKeepImporterTest {
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|
||||||
val actual = importer.parseToBaseNote(json)
|
val actual = with(importer) { json.parseToBaseNote() }
|
||||||
|
|
||||||
assertThat(actual.title).isEqualTo("Audio Note")
|
assertThat(actual.title).isEqualTo("Audio Note")
|
||||||
assertThat(actual.audios[0].name).isEqualTo("audio.3gp")
|
assertThat(actual.audios[0].name).isEqualTo("audio.3gp")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue