Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ interface BaseNoteDao {

@Query("SELECT * FROM BaseNote WHERE id = :id") fun get(id: Long): BaseNote?

@Query("SELECT * FROM BaseNote WHERE title = :title")
fun getByTitle(title: String): List<BaseNote>

@Query("SELECT images FROM BaseNote WHERE id = :id") fun getImages(id: Long): String

@Query("SELECT images FROM BaseNote") fun getAllImages(): List<String>
Expand Down
61 changes: 56 additions & 5 deletions app/src/main/java/com/philkes/notallyx/data/dao/CommonDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Transaction
import com.philkes.notallyx.data.NotallyDatabase
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
import com.philkes.notallyx.data.imports.ImportResult
import com.philkes.notallyx.data.model.BaseNote
import com.philkes.notallyx.data.model.Label
import com.philkes.notallyx.data.model.LabelsInBaseNote
Expand All @@ -13,6 +14,7 @@ import com.philkes.notallyx.data.model.createNoteUrl
import com.philkes.notallyx.data.model.getNoteIdFromUrl
import com.philkes.notallyx.data.model.getNoteTypeFromUrl
import com.philkes.notallyx.data.model.isNoteUrl
import com.philkes.notallyx.data.model.toText
import com.philkes.notallyx.utils.NoteSplitUtils

@Dao
Expand Down Expand Up @@ -44,17 +46,27 @@ abstract class CommonDao(private val database: NotallyDatabase) {
}

@Transaction
open suspend fun importBackup(baseNotes: List<BaseNote>, labels: List<Label>) {
open suspend fun importBackup(baseNotes: List<BaseNote>, labels: List<Label>): ImportResult {
val dao = database.getBaseNoteDao()
// Insert notes, splitting oversized text notes instead of truncating
var insertedCount = 0
var duplicates = 0
baseNotes.forEach { note ->
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
NoteSplitUtils.splitAndInsertForImport(note, dao)
// Skip duplicates: same title and same content
val duplicateId = findDuplicateId(note)
if (duplicateId == null) {
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
NoteSplitUtils.splitAndInsertForImport(note, dao)
} else {
dao.insert(note.copy(id = 0))
}
insertedCount++
} else {
dao.insert(note.copy(id = 0))
duplicates++
}
}
database.getLabelDao().insert(labels)
return ImportResult(inserted = insertedCount, duplicates = duplicates)
}

/**
Expand All @@ -67,16 +79,27 @@ abstract class CommonDao(private val database: NotallyDatabase) {
baseNotes: List<BaseNote>,
originalIds: List<Long>,
labels: List<Label>,
) {
): ImportResult {
val baseNoteDao = database.getBaseNoteDao()

// 1) Insert notes with splitting; build mapping from original id -> first-part new id
val idMap = HashMap<Long, Long>(originalIds.size)
// Keep all inserted note ids with their spans for remapping pass
val insertedParts = ArrayList<Pair<Long, List<SpanRepresentation>>>()

var insertedCount = 0
var duplicates = 0
for (i in baseNotes.indices) {
val original = baseNotes[i]
val duplicateId = findDuplicateId(original)
if (duplicateId != null) {
// Map the old id to the existing duplicate and do not insert
val oldId = originalIds.getOrNull(i)
if (oldId != null) idMap[oldId] = duplicateId
// No parts to update spans for existing notes
duplicates++
continue
}
val (firstId, parts) =
if (original.type == Type.NOTE && original.body.length > MAX_BODY_CHAR_LENGTH) {
NoteSplitUtils.splitAndInsertForImport(original, baseNoteDao)
Expand All @@ -87,6 +110,7 @@ abstract class CommonDao(private val database: NotallyDatabase) {
val oldId = originalIds.getOrNull(i)
if (oldId != null) idMap[oldId] = firstId
insertedParts.addAll(parts)
insertedCount++
}

// 2) Remap note links in spans for all inserted notes
Expand All @@ -111,5 +135,32 @@ abstract class CommonDao(private val database: NotallyDatabase) {
}

database.getLabelDao().insert(labels)
return ImportResult(inserted = insertedCount, duplicates = duplicates)
}

/**
* Returns the id of an existing note that has the same title and the same textual content as
* [note], or null if none found. For text notes, compares the body. For list notes, compares
* the textual representation of items (including checked state and hierarchy).
*/
private fun findDuplicateId(note: BaseNote): Long? {
val dao = database.getBaseNoteDao()
val titleMatches = dao.getByTitle(note.title)
if (titleMatches.isEmpty()) return null
val targetContent = normalizeContent(note)
return titleMatches
.firstOrNull { existing ->
existing.type == note.type && normalizeContent(existing) == targetContent
}
?.id
}

private fun normalizeContent(note: BaseNote): String {
val raw = if (note.type == Type.NOTE) note.body else note.items.toText()
return raw.replace("\r\n", "\n")
.replace("\r", "\n")
.trim()
.replace("\n+".toRegex(), "\n")
.replace("[\t ]+".toRegex(), " ")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.philkes.notallyx.data.model.Audio
import com.philkes.notallyx.data.model.FileAttachment
import com.philkes.notallyx.data.model.Label
import com.philkes.notallyx.data.model.Type
import com.philkes.notallyx.data.model.toText
import com.philkes.notallyx.presentation.viewmodel.NotallyModel
import com.philkes.notallyx.utils.MIME_TYPE_ZIP
import com.philkes.notallyx.utils.NoteSplitUtils
Expand All @@ -25,13 +26,15 @@ import com.philkes.notallyx.utils.backup.importImage
import java.io.File
import java.util.concurrent.atomic.AtomicInteger

data class ImportResult(val inserted: Int, val duplicates: Int)

class NotesImporter(private val app: Application, private val database: NotallyDatabase) {

suspend fun import(
uri: Uri,
importSource: ImportSource,
progress: MutableLiveData<ImportProgress>? = null,
): Int {
): ImportResult {
val tempDir = File(app.cacheDir, IMPORT_CACHE_FOLDER)
if (!tempDir.exists()) {
tempDir.mkdirs()
Expand Down Expand Up @@ -64,19 +67,28 @@ class NotesImporter(private val app: Application, private val database: NotallyD
importFiles(images, it, NotallyModel.FileType.IMAGE, progress, totalFiles, counter)
importAudios(audios, it, progress, totalFiles, counter)
}
// Insert notes with split handling for oversized text notes
// Insert notes with split handling for oversized text notes, skipping duplicates
val dao = database.getBaseNoteDao()
var insertedCount = 0
val totalCandidates = notes.size
notes.forEach { note ->
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
// Split into parts, preserving spans and adding navigation links
NoteSplitUtils.splitAndInsertForImport(note, dao)
} else {
// Regular insert; ensure id is auto-generated
dao.insert(note.copy(id = 0))
val dup = findDuplicateId(note)
if (dup == null) {
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
// Split into parts, preserving spans and adding navigation links
NoteSplitUtils.splitAndInsertForImport(note, dao)
} else {
// Regular insert; ensure id is auto-generated
dao.insert(note.copy(id = 0))
}
insertedCount++
}
}
progress?.postValue(ImportProgress(inProgress = false))
return notes.size
return ImportResult(
inserted = insertedCount,
duplicates = (totalCandidates - insertedCount),
)
} finally {
tempDir.deleteRecursively()
}
Expand Down Expand Up @@ -138,6 +150,27 @@ class NotesImporter(private val app: Application, private val database: NotallyD
private const val TAG = "NotesImporter"
const val IMPORT_CACHE_FOLDER = "imports"
}

private fun findDuplicateId(note: com.philkes.notallyx.data.model.BaseNote): Long? {
val dao = database.getBaseNoteDao()
val titleMatches = dao.getByTitle(note.title)
if (titleMatches.isEmpty()) return null
val target = normalizeContent(note)
return titleMatches
.firstOrNull { existing ->
existing.type == note.type && normalizeContent(existing) == target
}
?.id
}

private fun normalizeContent(note: com.philkes.notallyx.data.model.BaseNote): String {
val raw = if (note.type == Type.NOTE) note.body else note.items.toText()
return raw.replace("\r\n", "\n")
.replace("\r", "\n")
.trim()
.replace("\n+".toRegex(), "\n")
.replace("[\t ]+".toRegex(), " ")
}
}

enum class ImportSource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
}

viewModelScope.launch(exceptionHandler) {
val importedNotes =
val result =
withContext(Dispatchers.IO) {
val stream =
requireNotNull(
Expand All @@ -442,9 +442,12 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
)
val (baseNotes, labels) = stream.readAsBackup()
commonDao.importBackup(baseNotes, labels)
baseNotes.size
}
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
val baseMsg = app.getQuantityString(R.plurals.imported_notes, result.inserted)
val message =
if (result.duplicates > 0)
"$baseMsg (${app.getQuantityString(R.plurals.duplicates, result.duplicates)})"
else baseMsg
app.showToast(message)
}
}
Expand All @@ -461,11 +464,14 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
}

viewModelScope.launch(exceptionHandler) {
val importedNotes =
val result =
withContext(Dispatchers.IO) {
NotesImporter(app, database).import(uri, importSource, importProgress)
}
val message = app.getQuantityString(R.plurals.imported_notes, importedNotes)
val baseMsg = app.getQuantityString(R.plurals.imported_notes, result.inserted)
val message =
if (result.duplicates > 0) "$baseMsg (${result.duplicates} duplicates skipped)"
else baseMsg
app.showToast(message)
}
}
Expand Down Expand Up @@ -728,7 +734,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
viewModelScope.launch {
duplicateNotes(selected)
actionMode.close(true)
app.showToast(app.getQuantityString(R.plurals.duplicated_notes, selected.size))
app.showToast(app.getQuantityString(R.plurals.duplicates, selected.size))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ suspend fun ContextWrapper.importZip(
) {
importingBackup?.postValue(ImportProgress(indeterminate = true))
try {
val importedNotes =
val result =
withContext(Dispatchers.IO) {
val stream =
requireNotNull(
Expand Down Expand Up @@ -196,14 +196,19 @@ suspend fun ContextWrapper.importZip(

val notallyDatabase =
NotallyDatabase.getDatabase(this@importZip, observePreferences = false).value
notallyDatabase.getCommonDao().importBackup(baseNotes, originalIds, labels)
val importResult =
notallyDatabase.getCommonDao().importBackup(baseNotes, originalIds, labels)
val reminders = notallyDatabase.getBaseNoteDao().getAllReminders()
cancelNoteReminders(reminders)
scheduleNoteReminders(reminders)
baseNotes.size
importResult
}
databaseFolder.clearDirectory()
val message = getQuantityString(R.plurals.imported_notes, importedNotes)
val baseMsg = getQuantityString(R.plurals.imported_notes, result.inserted)
val message =
if (result.duplicates > 0)
"$baseMsg (${getQuantityString(R.plurals.duplicates, result.duplicates)})"
else baseMsg
showToast(message)
} catch (e: ZipException) {
if (e.type == ZipException.Type.WRONG_PASSWORD) {
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@
<string name="donate">Make a Donation</string>
<string name="drag_handle">Drag Handle</string>
<string name="duplicate">Duplicate</string>
<plurals name="duplicated_notes">
<item quantity="one">Duplicated %1$d note</item>
<item quantity="other">Duplicated %1$d notes</item>
<plurals name="duplicates">
<item quantity="one">%1$d duplicate</item>
<item quantity="other">%1$d duplicates</item>
</plurals>
<string name="edit">Edit</string>
<string name="edit_color">Edit Color</string>
Expand Down
Binary file modified app/translations.xlsx
Binary file not shown.