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
60 changes: 30 additions & 30 deletions TRANSLATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,34 @@ See [Android Translations Converter](https://github.com/Crustack/android-transla
<!-- translations:start -->
| Language | Coverage |
|----------|----------|
| 🇺🇸 English | 100% (320/320) |
| 🇪🇸 Catalan | 20% (65/320) |
| 🇨🇿 Czech | 97% (313/320) |
| 🇩🇰 Danish | 21% (69/320) |
| 🇩🇪 German | 97% (313/320) |
| 🇬🇷 Greek | 22% (72/320) |
| 🇪🇸 Spanish | 97% (313/320) |
| 🇫🇷 French | 94% (301/320) |
| 🇭🇺 Hungarian | 20% (65/320) |
| 🇮🇩 Indonesian | 23% (75/320) |
| 🇮🇹 Italian | 90% (291/320) |
| 🇯🇵 Japanese | 22% (73/320) |
| 🇲🇲 Burmese | 28% (90/320) |
| 🇳🇴 Norwegian Bokmål | 33% (106/320) |
| 🇳🇱 Dutch | 66% (212/320) |
| 🇳🇴 Norwegian Nynorsk | 33% (106/320) |
| 🇵🇱 Polish | 93% (300/320) |
| 🇧🇷 Portuguese (Brazil) | 97% (312/320) |
| 🇵🇹 Portuguese (Portugal) | 22% (71/320) |
| 🇷🇴 Romanian | 94% (301/320) |
| 🇷🇺 Russian | 95% (305/320) |
| 🇸🇰 Slovak | 20% (65/320) |
| 🇸🇮 Slovenian | 34% (109/320) |
| 🇸🇪 Swedish | 19% (63/320) |
| 🇵🇭 Tagalog | 20% (65/320) |
| 🇹🇷 Turkish | 22% (73/320) |
| 🇺🇦 Ukrainian | 98% (314/320) |
| 🇻🇳 Vietnamese | 33% (107/320) |
| 🇨🇳 Chinese (Simplified) | 97% (312/320) |
| 🇹🇼 Chinese (Traditional) | 91% (294/320) |
| 🇺🇸 English | 100% (326/326) |
| 🇪🇸 Catalan | 19% (65/326) |
| 🇨🇿 Czech | 96% (313/326) |
| 🇩🇰 Danish | 21% (69/326) |
| 🇩🇪 German | 96% (313/326) |
| 🇬🇷 Greek | 22% (72/326) |
| 🇪🇸 Spanish | 96% (313/326) |
| 🇫🇷 French | 92% (301/326) |
| 🇭🇺 Hungarian | 19% (65/326) |
| 🇮🇩 Indonesian | 23% (75/326) |
| 🇮🇹 Italian | 89% (291/326) |
| 🇯🇵 Japanese | 22% (73/326) |
| 🇲🇲 Burmese | 27% (90/326) |
| 🇳🇴 Norwegian Bokmål | 32% (106/326) |
| 🇳🇱 Dutch | 65% (212/326) |
| 🇳🇴 Norwegian Nynorsk | 32% (106/326) |
| 🇵🇱 Polish | 92% (300/326) |
| 🇧🇷 Portuguese (Brazil) | 95% (312/326) |
| 🇵🇹 Portuguese (Portugal) | 21% (71/326) |
| 🇷🇴 Romanian | 92% (301/326) |
| 🇷🇺 Russian | 93% (305/326) |
| 🇸🇰 Slovak | 19% (65/326) |
| 🇸🇮 Slovenian | 33% (109/326) |
| 🇸🇪 Swedish | 19% (63/326) |
| 🇵🇭 Tagalog | 19% (65/326) |
| 🇹🇷 Turkish | 22% (73/326) |
| 🇺🇦 Ukrainian | 96% (314/326) |
| 🇻🇳 Vietnamese | 32% (107/326) |
| 🇨🇳 Chinese (Simplified) | 95% (312/326) |
| 🇹🇼 Chinese (Traditional) | 90% (294/326) |
<!-- translations:end -->
2 changes: 0 additions & 2 deletions app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import com.philkes.notallyx.utils.backup.isEqualTo
import com.philkes.notallyx.utils.backup.modifiedNoteBackupExists
import com.philkes.notallyx.utils.backup.scheduleAutoBackup
import com.philkes.notallyx.utils.backup.updateAutoBackup
import com.philkes.notallyx.utils.checkForMigrations
import com.philkes.notallyx.utils.observeOnce
import com.philkes.notallyx.utils.security.UnlockReceiver
import java.util.concurrent.TimeUnit
Expand All @@ -53,7 +52,6 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
registerActivityLifecycleCallbacks(this)
if (isTestRunner()) return
preferences = NotallyXPreferences.getInstance(this)
checkForMigrations()
if (preferences.useDynamicColors.value) {
if (DynamicColors.isDynamicColorAvailable()) {
DynamicColors.applyToActivitiesIfAvailable(this)
Expand Down
69 changes: 69 additions & 0 deletions app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.philkes.notallyx.data.dao

import android.content.ContextWrapper
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.room.Dao
Expand All @@ -9,6 +10,7 @@ import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.philkes.notallyx.R
import com.philkes.notallyx.data.model.Audio
import com.philkes.notallyx.data.model.BaseNote
import com.philkes.notallyx.data.model.FileAttachment
Expand All @@ -17,6 +19,10 @@ import com.philkes.notallyx.data.model.LabelsInBaseNote
import com.philkes.notallyx.data.model.ListItem
import com.philkes.notallyx.data.model.Reminder
import com.philkes.notallyx.data.model.Type
import com.philkes.notallyx.presentation.showToast
import com.philkes.notallyx.utils.charLimit
import com.philkes.notallyx.utils.log
import kotlin.text.compareTo

data class NoteIdReminder(val id: Long, val reminders: List<Reminder>)

Expand All @@ -27,13 +33,66 @@ data class NoteReminder(
val reminders: List<Reminder>,
)

/** Maximum allowed size of a note body in MB (~340,000 characters) */
const val MAX_BODY_SIZE_MB = 1.5

@Dao
interface BaseNoteDao {

@RawQuery fun query(query: SupportSQLiteQuery): Int

@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(baseNote: BaseNote): Long

private fun BaseNote.truncated(): Pair<Boolean, BaseNote> {
return if (body.length > MAX_BODY_CHAR_LENGTH) {
return Pair(true, copy(body = body.take(MAX_BODY_CHAR_LENGTH)))
} else Pair(false, this)
Comment on lines +46 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clamp spans when truncating bodies.

Body truncation without span clipping can leave out-of-bounds spans and corrupt rendering.

🛠️ Suggested fix
-import com.philkes.notallyx.utils.charLimit
+import com.philkes.notallyx.utils.charLimit
+import com.philkes.notallyx.utils.NoteSplitUtils.sliceSpans
@@
     private fun BaseNote.truncated(): Pair<Boolean, BaseNote> {
-        return if (body.length > MAX_BODY_CHAR_LENGTH) {
-            return Pair(true, copy(body = body.take(MAX_BODY_CHAR_LENGTH)))
-        } else Pair(false, this)
+        return if (body.length > MAX_BODY_CHAR_LENGTH) {
+            val newBody = body.take(MAX_BODY_CHAR_LENGTH)
+            val newSpans =
+                if (spans.isNotEmpty()) sliceSpans(spans, 0, newBody.length, 0) else emptyList()
+            Pair(true, copy(body = newBody, spans = newSpans))
+        } else Pair(false, this)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun BaseNote.truncated(): Pair<Boolean, BaseNote> {
return if (body.length > MAX_BODY_CHAR_LENGTH) {
return Pair(true, copy(body = body.take(MAX_BODY_CHAR_LENGTH)))
} else Pair(false, this)
private fun BaseNote.truncated(): Pair<Boolean, BaseNote> {
return if (body.length > MAX_BODY_CHAR_LENGTH) {
val newBody = body.take(MAX_BODY_CHAR_LENGTH)
val newSpans =
if (spans.isNotEmpty()) sliceSpans(spans, 0, newBody.length, 0) else emptyList()
Pair(true, copy(body = newBody, spans = newSpans))
} else Pair(false, this)
}
🤖 Prompt for AI Agents
In `@app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt` around lines
46 - 49, The truncation in BaseNote.truncated currently slices body but leaves
spans intact; update truncated() so when body.length > MAX_BODY_CHAR_LENGTH you
not only copy(body = body.take(MAX_BODY_CHAR_LENGTH)) but also clamp/clip the
note's spans to the new length (remove spans that start >= newLen and trim spans
that partially exceed newLen by reducing their length to fit), returning
Pair(true, copy(body = truncatedBody, spans = clippedSpans)); keep
MAX_BODY_CHAR_LENGTH and BaseNote.truncated as the reference points for the
change.

}

suspend fun insertSafe(context: ContextWrapper, baseNote: BaseNote): Long {
val (truncated, note) = baseNote.truncated()
if (truncated) {
context.log(
TAG,
"Note (id: ${note.id}, title: '${note.title}') is too big to insert. Truncating to ${note.body.length} characters (was: ${baseNote.body.length})",
)
context.showToast(
context.getString(
R.string.note_too_big_truncating,
note.body.length,
baseNote.body.length,
)
)
}
return insert(note)
}

suspend fun insertSafe(context: ContextWrapper, baseNotes: List<BaseNote>): List<Long> {
val truncatedNotes = mutableListOf<BaseNote>()
var truncatedCharacterSize = 0
val notes =
baseNotes.map { baseNote ->
val (truncated, note) = baseNote.truncated()
if (truncated) {
truncatedCharacterSize += baseNote.body.length
truncatedNotes.add(note)
}
note
}
context.log(
TAG,
"${truncatedNotes.size} Notes are too big to save, they were truncated to $truncatedCharacterSize characters",
)
context.showToast(
context.getString(
R.string.notes_too_big_truncating,
truncatedNotes.size,
truncatedCharacterSize,
)
)
return insert(notes)
Comment on lines +70 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid logging/toasting when nothing was truncated and fix size count.

The current implementation logs and shows a toast even when no note was truncated, and the size metric uses the original length rather than the truncated length.

🛠️ Suggested fix
         val notes =
             baseNotes.map { baseNote ->
                 val (truncated, note) = baseNote.truncated()
                 if (truncated) {
-                    truncatedCharacterSize += baseNote.body.length
+                    truncatedCharacterSize += note.body.length
                     truncatedNotes.add(note)
                 }
                 note
             }
-        context.log(
-            TAG,
-            "${truncatedNotes.size} Notes are too big to save, they were truncated to $truncatedCharacterSize characters",
-        )
-        context.showToast(
-            context.getString(
-                R.string.notes_too_big_truncating,
-                truncatedNotes.size,
-                truncatedCharacterSize,
-            )
-        )
+        if (truncatedNotes.isNotEmpty()) {
+            context.log(
+                TAG,
+                "${truncatedNotes.size} Notes are too big to save, they were truncated to $truncatedCharacterSize characters",
+            )
+            context.showToast(
+                context.getString(
+                    R.string.notes_too_big_truncating,
+                    truncatedNotes.size,
+                    truncatedCharacterSize,
+                )
+            )
+        }
         return insert(notes)
🤖 Prompt for AI Agents
In `@app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt` around lines
70 - 93, In insertSafe, only log and show the toast when something was actually
truncated: guard the context.log and context.showToast calls with a check like
if (truncatedNotes.isNotEmpty()) to avoid spurious messages; also fix the size
accounting by adding the truncated note's length (use note.body.length) to
truncatedCharacterSize instead of baseNote.body.length so truncatedCharacterSize
reflects the stored/truncated size, not the original size.

}

@Insert suspend fun insert(baseNotes: List<BaseNote>): List<Long>

@Update(entity = BaseNote::class) suspend fun update(labelsInBaseNotes: List<LabelsInBaseNote>)
Expand Down Expand Up @@ -135,6 +194,10 @@ interface BaseNoteDao {
spans: List<com.philkes.notallyx.data.model.SpanRepresentation>,
)

// Truncate body at DB level without loading the row, to resolve oversized rows safely
@Query("UPDATE BaseNote SET body = substr(body, 1, :limit) WHERE id = :id")
suspend fun truncateBody(id: Long, limit: Int)

/**
* Both id and position can be invalid.
*
Expand Down Expand Up @@ -248,4 +311,10 @@ interface BaseNoteDao {
}
return false
}

companion object {
private const val TAG = "BaseNoteDao"

val MAX_BODY_CHAR_LENGTH = MAX_BODY_SIZE_MB.charLimit()
}
}
59 changes: 42 additions & 17 deletions app/src/main/java/com/philkes/notallyx/data/dao/CommonDao.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.philkes.notallyx.data.dao

import android.content.ContextWrapper
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.model.BaseNote
import com.philkes.notallyx.data.model.Label
import com.philkes.notallyx.data.model.LabelsInBaseNote
import com.philkes.notallyx.data.model.SpanRepresentation
import com.philkes.notallyx.data.model.Type
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.utils.NoteSplitUtils

@Dao
abstract class CommonDao(private val database: NotallyDatabase) {
Expand Down Expand Up @@ -40,8 +45,20 @@ abstract class CommonDao(private val database: NotallyDatabase) {
}

@Transaction
open suspend fun importBackup(baseNotes: List<BaseNote>, labels: List<Label>) {
database.getBaseNoteDao().insert(baseNotes)
open suspend fun importBackup(
context: ContextWrapper,
baseNotes: List<BaseNote>,
labels: List<Label>,
) {
val dao = database.getBaseNoteDao()
// Insert notes, splitting oversized text notes instead of truncating
baseNotes.forEach { note ->
if (note.type == Type.NOTE && note.body.length > MAX_BODY_CHAR_LENGTH) {
NoteSplitUtils.splitAndInsertForImport(note, dao)
} else {
dao.insert(note.copy(id = 0))
}
}
Comment on lines +48 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify splitAndInsertForImport signature and importBackup usage

echo "=== Checking splitAndInsertForImport signature ==="
rg -n "fun splitAndInsertForImport" --type=kt -A5

echo ""
echo "=== Checking all calls to importBackup ==="
rg -n "importBackup" --type=kt -B2 -A2

echo ""
echo "=== Checking if context is used anywhere else in importBackup method ==="
# Get the full importBackup method to check for context usage
rg -n "open suspend fun importBackup" --type=kt -A20

Repository: Crustack/NotallyX

Length of output: 327


🏁 Script executed:

#!/bin/bash
# Find Kotlin files and search for the relevant signatures

echo "=== Finding Kotlin files ==="
fd -e kt | head -20

echo ""
echo "=== Checking splitAndInsertForImport signature ==="
rg "fun splitAndInsertForImport" -A5

echo ""
echo "=== Checking all calls to importBackup ==="
rg "importBackup" -B2 -A2

echo ""
echo "=== Checking the full importBackup method signature ==="
rg "open suspend fun importBackup" -A30

Repository: Crustack/NotallyX

Length of output: 50373


Remove unused context parameter from importBackup.

The context: ContextWrapper parameter is declared but never used. NoteSplitUtils.splitAndInsertForImport(note, dao) doesn't accept a context parameter, and no other code in the method references it. Either remove the parameter or confirm if it's intended for future use.

🤖 Prompt for AI Agents
In `@app/src/main/java/com/philkes/notallyx/data/dao/CommonDao.kt` around lines 48
- 61, The importBackup method in CommonDao declares an unused context:
ContextWrapper parameter; remove this unused parameter from the open suspend fun
importBackup signature and update all implementations/overrides and call sites
to match the new signature. Ensure the method body still references
database.getBaseNoteDao(), MAX_BODY_CHAR_LENGTH,
NoteSplitUtils.splitAndInsertForImport(note, dao) and dao.insert(note.copy(id =
0)) unchanged; if any external interface or tests declared the old signature,
update them accordingly.

database.getLabelDao().insert(labels)
}

Expand All @@ -57,21 +74,31 @@ abstract class CommonDao(private val database: NotallyDatabase) {
labels: List<Label>,
) {
val baseNoteDao = database.getBaseNoteDao()
val newIds = baseNoteDao.insert(baseNotes)
// Build old->new mapping using positional correspondence

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

// Remap note links in spans where necessary
for (i in baseNotes.indices) {
val note = baseNotes[i]
val newId = newIds.getOrNull(i) ?: continue
val original = baseNotes[i]
val (firstId, parts) =
if (original.type == Type.NOTE && original.body.length > MAX_BODY_CHAR_LENGTH) {
NoteSplitUtils.splitAndInsertForImport(original, baseNoteDao)
} else {
val newId = baseNoteDao.insert(original.copy(id = 0))
Pair(newId, listOf(Pair(newId, original.spans)))
}
val oldId = originalIds.getOrNull(i)
if (oldId != null) idMap[oldId] = firstId
insertedParts.addAll(parts)
}

// 2) Remap note links in spans for all inserted notes
for ((noteId, spans) in insertedParts) {
var changed = false
val updatedSpans =
note.spans.map { span ->
val updated =
spans.map { span ->
if (span.link && span.linkData?.isNoteUrl() == true) {
val url = span.linkData!!
val oldTargetId = url.getNoteIdFromUrl()
Expand All @@ -80,13 +107,11 @@ abstract class CommonDao(private val database: NotallyDatabase) {
if (newTargetId != null) {
changed = true
span.copy(linkData = newTargetId.createNoteUrl(type))
} else {
span
}
} else span
} else span
}
if (changed) {
baseNoteDao.updateSpans(newId, updatedSpans)
baseNoteDao.updateSpans(noteId, updated)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import com.philkes.notallyx.R
import com.philkes.notallyx.data.NotallyDatabase
import com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter
import com.philkes.notallyx.data.imports.google.GoogleKeepImporter
import com.philkes.notallyx.data.imports.txt.JsonImporter
import com.philkes.notallyx.data.imports.txt.PlainTextImporter
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.presentation.viewmodel.NotallyModel
import com.philkes.notallyx.utils.MIME_TYPE_ZIP
import com.philkes.notallyx.utils.NoteSplitUtils
import com.philkes.notallyx.utils.backup.importAudio
import com.philkes.notallyx.utils.backup.importFile
import com.philkes.notallyx.utils.backup.importImage
Expand Down Expand Up @@ -61,7 +64,17 @@ 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)
}
database.getBaseNoteDao().insert(notes)
// Insert notes with split handling for oversized text notes
val dao = database.getBaseNoteDao()
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))
}
}
progress?.postValue(ImportProgress(inProgress = false))
return notes.size
} finally {
Expand Down
Loading