Skip to content
Draft
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
511 changes: 511 additions & 0 deletions app/schemas/com.ethran.notable.data.db.AppDatabase/35.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions app/src/main/java/com/ethran/notable/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.ethran.notable.data.datastore.GlobalAppSettings
import com.ethran.notable.data.db.KvProxy
import com.ethran.notable.data.db.reencodeStrokePointsToSB1
import com.ethran.notable.editor.DrawCanvas
import com.ethran.notable.io.IndexExporter
import com.ethran.notable.ui.LocalSnackContext
import com.ethran.notable.ui.Router
import com.ethran.notable.ui.SnackBar
Expand Down Expand Up @@ -85,10 +86,13 @@ class MainActivity : ComponentActivity() {
this.lifecycleScope.launch(Dispatchers.IO) {
reencodeStrokePointsToSB1(this@MainActivity)
}
// Export index for external tools (e.g., Emacs integration)
IndexExporter.scheduleExport(this)
}

//EpdDeviceManager.enterAnimationUpdate(true);
// val intentData = intent.data?.lastPathSegment
// Extract deep link data from intent (e.g., notable://page-{id})
val intentData = intent.data?.toString()

setContent {
InkaTheme {
Expand All @@ -97,7 +101,7 @@ class MainActivity : ComponentActivity() {
Modifier
.background(Color.White)
) {
Router()
Router(intentData = intentData)
}
Box(
Modifier
Expand Down Expand Up @@ -126,9 +130,12 @@ class MainActivity : ComponentActivity() {
super.onPause()
this.lifecycleScope.launch {
Log.d("QuickSettings", "App is paused - maybe quick settings opened?")

DrawCanvas.refreshUi.emit(Unit)
}
// Export index when app goes to background
if (hasFilePermission(this)) {
IndexExporter.scheduleExport(this)
}
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ data class AppSettings(
val twoFingerSwipeLeftAction: GestureAction? = defaultTwoFingerSwipeLeftAction,
val twoFingerSwipeRightAction: GestureAction? = defaultTwoFingerSwipeRightAction,
val holdAction: GestureAction? = defaultHoldAction,
val continuousStrokeSlider: Boolean = false,

) {
val continuousStrokeSlider: Boolean = false
) {
companion object {
val defaultDoubleTapAction get() = GestureAction.Undo
val defaultTwoFingerTapAction get() = GestureAction.ChangeTool
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/ethran/notable/data/db/Db.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Converters {

@Database(
entities = [Folder::class, Notebook::class, Page::class, Stroke::class, Image::class, Kv::class],
version = 34,
version = 35,
autoMigrations = [
AutoMigration(19, 20),
AutoMigration(20, 21),
Expand All @@ -63,7 +63,8 @@ class Converters {
AutoMigration(30, 31, spec = AutoMigration30to31::class),
AutoMigration(31, 32, spec = AutoMigration31to32::class),
AutoMigration(32, 33),
AutoMigration(33, 34)
AutoMigration(33, 34),
AutoMigration(34, 35)
], exportSchema = true
)
@TypeConverters(Converters::class)
Expand Down
15 changes: 13 additions & 2 deletions app/src/main/java/com/ethran/notable/data/db/Folder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ interface FolderDao {
@Query("SELECT * FROM folder WHERE id IS :folderId")
fun get(folderId: String): Folder

@Query("SELECT * FROM folder")
fun getAll(): List<Folder>

@Query("SELECT * FROM folder WHERE title = :title LIMIT 1")
fun getByTitle(title: String): Folder?
Comment on lines +48 to +49
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The getByTitle query only returns one folder when multiple folders with the same title can exist. This will cause unpredictable behavior when creating pages in folders via deep links, as the query will arbitrarily select one matching folder. Consider using a combination of folder name and parent path to uniquely identify folders, or document that folder names must be unique in the documentation.

Copilot uses AI. Check for mistakes.

@Insert
fun create(folder: Folder): Long
Expand All @@ -64,10 +69,18 @@ class FolderRepository(context: Context) {
db.update(folder)
}

fun getAll(): List<Folder> {
return db.getAll()
}

fun getAllInFolder(folderId: String? = null): LiveData<List<Folder>> {
return db.getChildrenFolders(folderId)
}

fun getByTitle(title: String): Folder? {
return db.getByTitle(title)
}

fun getParent(folderId: String? = null): String? {
if (folderId == null)
return null
Expand All @@ -79,9 +92,7 @@ class FolderRepository(context: Context) {
return db.get(folderId)
}


fun delete(id: String) {
db.delete(id)
}

}
5 changes: 3 additions & 2 deletions app/src/main/java/com/ethran/notable/data/db/Kv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,20 @@ class KvRepository(context: Context) {

class KvProxy(context: Context) {
private val kvRepository = KvRepository(context)
private val json = Json { ignoreUnknownKeys = true }

fun <T> observeKv(key: String, serializer: KSerializer<T>, default: T): LiveData<T?> {
return kvRepository.getLive(key).map {
if (it == null) return@map default
val jsonValue = it.value
Json.decodeFromString(serializer, jsonValue)
json.decodeFromString(serializer, jsonValue)
}
}

fun <T> get(key: String, serializer: KSerializer<T>): T? {
val kv = kvRepository.get(key) ?: return null //returns null when there is no database
val jsonValue = kv.value
return Json.decodeFromString(serializer, jsonValue)
return json.decodeFromString(serializer, jsonValue)
}


Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/com/ethran/notable/data/db/Notebook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ interface NotebookDao {
@Query("SELECT * FROM notebook WHERE parentFolderId is :folderId")
fun getAllInFolder(folderId: String? = null): LiveData<List<Notebook>>

@Query("SELECT * FROM notebook")
fun getAll(): List<Notebook>

@Query("SELECT * FROM notebook WHERE id = (:notebookId)")
fun getByIdLive(notebookId: String): LiveData<Notebook>

Expand Down Expand Up @@ -100,6 +103,10 @@ class BookRepository(context: Context) {
db.update(updatedNotebook)
}

fun getAll(): List<Notebook> {
return db.getAll()
}

fun getAllInFolder(folderId: String? = null): LiveData<List<Notebook>> {
return db.getAllInFolder(folderId)
}
Expand Down
14 changes: 12 additions & 2 deletions app/src/main/java/com/ethran/notable/data/db/Page.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ import java.util.UUID
)]
)
data class Page(
@PrimaryKey val id: String = UUID.randomUUID().toString(), val scroll: Int = 0,
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val name: String? = null,
Copy link
Owner

Choose a reason for hiding this comment

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

is this the only think that changed in db, that requires migration?

I don't see exactly why its needed, where its used. For now, I mostly generated page names to contain "quick pages" or page number in notebook.

It might be useful to have names for separate pages in notebook, but for now I don't see a need for it.

Copy link
Author

@dragarok dragarok Feb 2, 2026

Choose a reason for hiding this comment

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

Quick pages don't have names and can pile up without us understanding what's under the hood. Previous logic works quite well and this doesn't change anything for the end user. If they want to rename manually, then they can. Else, it is exactly the same. And doesn't disrupt older database either.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, seems ok.

val scroll: Int = 0,
@ColumnInfo(index = true) val notebookId: String? = null,
@ColumnInfo(defaultValue = "blank") val background: String = "blank", // path or native subtype
@ColumnInfo(defaultValue = "native") val backgroundType: String = "native", // image, imageRepeating, coverImage, native
@ColumnInfo(index = true) val parentFolderId: String? = null,
val createdAt: Date = Date(), val updatedAt: Date = Date()
val createdAt: Date = Date(),
val updatedAt: Date = Date()
)

data class PageWithStrokes(
Expand All @@ -58,6 +61,9 @@ interface PageDao {
@Query("SELECT * FROM page WHERE id IN (:ids)")
fun getByIds(ids: List<String>): List<Page>

@Query("SELECT * FROM page")
fun getAll(): List<Page>

@Query("SELECT * FROM page WHERE id = (:pageId)")
fun getById(pageId: String): Page?

Expand Down Expand Up @@ -100,6 +106,10 @@ class PageRepository(context: Context) {
return db.updateScroll(id, scroll)
}

fun getAll(): List<Page> {
return db.getAll()
}

fun getById(pageId: String): Page? {
return db.getById(pageId)
}
Expand Down
118 changes: 118 additions & 0 deletions app/src/main/java/com/ethran/notable/editor/ui/PageMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@ package com.ethran.notable.editor.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import com.ethran.notable.data.AppRepository
Expand All @@ -33,6 +48,19 @@ fun PageMenu(
) {
val context = LocalContext.current
val appRepository = AppRepository(context)
var showRenameDialog by remember { mutableStateOf(false) }

if (showRenameDialog) {
PageRenameDialog(
pageId = pageId,
appRepository = appRepository,
onClose = {
showRenameDialog = false
onClose()
}
)
return
}
Popup(
alignment = Alignment.TopStart,
onDismissRequest = { onClose() },
Expand Down Expand Up @@ -85,6 +113,15 @@ fun PageMenu(
}
}

Box(
Modifier
.padding(10.dp)
.noRippleClickable {
showRenameDialog = true
}) {
Text("Rename")
}

Box(
Modifier
.padding(10.dp)
Expand All @@ -107,3 +144,84 @@ fun PageMenu(
}
}

@Composable
fun PageRenameDialog(
pageId: String,
appRepository: AppRepository,
onClose: () -> Unit
) {
val page = remember { appRepository.pageRepository.getById(pageId) }
var pageName by remember { mutableStateOf(page?.name ?: "") }
Comment on lines +153 to +154
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The page is fetched once during initialization but is not updated if the page is modified elsewhere. If the page is deleted or modified by another process before the user clicks Save, the update will fail silently or use stale data. Consider using a mutableState with a snapshot or re-fetching the page before updating.

Copilot uses AI. Check for mistakes.

Dialog(onDismissRequest = onClose) {
Column(
modifier = Modifier
.background(Color.White)
.border(1.dp, Color.Black, RectangleShape)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Rename Page",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)

BasicTextField(
value = pageName,
onValueChange = { pageName = it },
textStyle = TextStyle(fontSize = 16.sp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
page?.let {
appRepository.pageRepository.update(it.copy(name = pageName.ifBlank { null }))
}
onClose()
}
),
modifier = Modifier
.fillMaxWidth()
.border(1.dp, Color.Gray, RectangleShape)
.padding(12.dp),
decorationBox = { innerTextField ->
Box {
if (pageName.isEmpty()) {
Text("Page name", color = Color.Gray)
}
innerTextField()
}
}
)

Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(top = 12.dp)
) {
Box(
Modifier
.border(1.dp, Color.Black, RectangleShape)
.padding(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable { onClose() }
) {
Text("Cancel")
}
Box(
Modifier
.border(1.dp, Color.Black, RectangleShape)
.padding(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable {
page?.let {
appRepository.pageRepository.update(it.copy(name = pageName.ifBlank { null }))
}
onClose()
}
) {
Text("Save")
}
}
}
}
}

Loading