diff --git a/alembic/versions/0025_drop_note_kind.py b/alembic/versions/0025_drop_note_kind.py
new file mode 100644
index 0000000..08fd2d0
--- /dev/null
+++ b/alembic/versions/0025_drop_note_kind.py
@@ -0,0 +1,35 @@
+"""drop notes.kind — a checklist is something a note HAS (M13 step 2)
+
+Revision ID: 0025
+Revises: 0024
+Create Date: 2026-08-22
+
+`kind` was never a type: a plain TEXT column with no enum and no CHECK, compared
+against a hardcoded ("text", "list") tuple in six places. `note_items` was always an
+ordinary child table keyed by note_id, serialization always emitted `items` whatever
+the kind, and the Android editor already toggled between the two losslessly. The
+storage has modelled "a body plus optional checkable items" the whole time; only the
+gates forbade it.
+
+Nothing is lost. Items were already rows in their own table, and a note that was
+`kind = 'list'` keeps every one of them — it just stops being a different sort of
+thing from the note next to it.
+"""
+from alembic import op
+import sqlalchemy as sa
+
+revision = "0025"
+down_revision = "0024"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.drop_column("notes", "kind")
+
+
+def downgrade() -> None:
+ # server_default so existing rows get a value; every note comes back as 'text',
+ # which is right — a restored note with items would previously have hidden its
+ # body, and there is no record of which ones were once lists.
+ op.add_column("notes", sa.Column("kind", sa.Text(), nullable=False, server_default="text"))
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/MainActivity.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/MainActivity.kt
index f88d0f8..4ee1ef2 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/MainActivity.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/MainActivity.kt
@@ -226,8 +226,8 @@ private fun App(
ComposeSheet(
saving = board.state.saving,
onDismiss = { composing = false },
- onSave = { kind, title, content ->
- board.create(kind, title, content)
+ onSave = { title, content ->
+ board.create(title, content)
composing = false
},
)
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt
index 037310e..146dd27 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt
@@ -50,9 +50,6 @@ sealed interface Destination {
) : Destination
}
-/** What kind of thing the compose sheet is making. */
-enum class DraftKind { NOTE, LIST }
-
/** Everything the board renders from, in one immutable snapshot. */
data class BoardState(
val destination: Destination = Destination.Notes,
@@ -198,7 +195,6 @@ class BoardViewModel(
* mistake worth interrupting someone over.
*/
fun create(
- kind: DraftKind,
title: String,
content: String,
) {
@@ -210,7 +206,7 @@ class BoardViewModel(
state = state.copy(saving = true)
state =
try {
- val created = withContext(Dispatchers.IO) { core.createNote(draft(kind, cleanTitle, cleanContent)) }
+ val created = withContext(Dispatchers.IO) { core.createNote(draft(cleanTitle, cleanContent)) }
// Prepend rather than reload: the new note belongs at the top
// of the board, and a full re-query would cost a round trip to
// tell us what we already know. Skipped when the board is not
@@ -306,9 +302,6 @@ class BoardViewModel(
is EditorAction.SetColor -> edit(id, NoteEdit.Color(action.color))
- EditorAction.ToggleKind ->
- edit(id, NoteEdit.Kind(if (note.kind == KIND_LIST) KIND_TEXT else KIND_LIST))
-
// Pinning re-sorts the board rather than emptying it, and on a phone
// you often pin while still reading — so unlike the three below, it
// deliberately leaves the editor open.
@@ -331,6 +324,10 @@ class BoardViewModel(
null
}
+ // An empty first item: the checklist editor appears the moment the note
+ // has one, and an empty row is what someone can type straight into.
+ EditorAction.AddChecklist -> mutate { it.addItem(id, "") }
+
is EditorAction.AddItem ->
action.text.trim().takeIf { it.isNotEmpty() }?.let { text ->
mutate { it.addItem(id, text) }
@@ -468,25 +465,11 @@ private fun query(
) = NoteQuery(view = view, labelId = labelId, sort = null, facets = null)
private fun draft(
- kind: DraftKind,
title: String,
content: String,
): NoteDraft =
- when (kind) {
- // Body left to carry the text; the core derives display_title from its
- // first line when no title was given, so a captured thought is nameable
- // without making the user name it.
- DraftKind.NOTE ->
- NoteDraft(title = title, body = content, color = DEFAULT_COLOR, kind = null, items = null)
- // One line per item. At CAPTURE time the whole list is already in your
- // head, so typing it in one go beats a tap between each row; the editor
- // has the per-row control for when the list is being revised instead.
- DraftKind.LIST ->
- NoteDraft(
- title = title,
- body = "",
- color = DEFAULT_COLOR,
- kind = KIND_LIST,
- items = content.lines().map { it.trim() }.filter { it.isNotEmpty() },
- )
- }
+ // Body carries the text; the core derives display_title from its first line when
+ // no title was given, so a captured thought is nameable without making the user
+ // name it. A checklist is added afterwards, in the editor — it is something a note
+ // HAS, not a different thing to capture (M13 step 2).
+ NoteDraft(title = title, body = content, color = DEFAULT_COLOR, items = null)
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/ComposeSheet.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/ComposeSheet.kt
index 721b275..1894b32 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/ComposeSheet.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/ComposeSheet.kt
@@ -58,18 +58,17 @@ import com.fabledsword.thoughtsync.R
fun ComposeSheet(
saving: Boolean,
onDismiss: () -> Unit,
- onSave: (DraftKind, String, String) -> Unit,
+ onSave: (String, String) -> Unit,
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// Saveable, not just remembered: a rotation mid-sentence is the same lost
// thought as a discarded one, and it was losing it before this.
- var kind by rememberSaveable { mutableStateOf(DraftKind.NOTE) }
var title by rememberSaveable { mutableStateOf("") }
var content by rememberSaveable { mutableStateOf("") }
val contentFocus = remember { FocusRequester() }
val written = title.isNotBlank() || content.isNotBlank()
- val leave = { if (written) onSave(kind, title, content) else onDismiss() }
+ val leave = { if (written) onSave(title, content) else onDismiss() }
// Land in the body, not the title. Most captures are a thought, not a titled
// document, and making someone tab past an optional field is the difference
@@ -79,7 +78,7 @@ fun ComposeSheet(
// Backgrounding PERSISTS but does not close an empty sheet. Someone who tapped
// + and then got distracted should find the composer where they left it; the
// only reason to act here is that there is something to lose.
- FlushOnStop { if (written) onSave(kind, title, content) }
+ FlushOnStop { if (written) onSave(title, content) }
ModalBottomSheet(onDismissRequest = leave, sheetState = sheetState) {
Column(
@@ -91,19 +90,8 @@ fun ComposeSheet(
.navigationBarsPadding(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- FilterChip(
- selected = kind == DraftKind.NOTE,
- onClick = { kind = DraftKind.NOTE },
- label = { Text(stringResource(R.string.compose_kind_note)) },
- )
- FilterChip(
- selected = kind == DraftKind.LIST,
- onClick = { kind = DraftKind.LIST },
- label = { Text(stringResource(R.string.compose_kind_list)) },
- )
- }
-
+ // No note/list switch any more: there is one thing to capture. A
+ // checklist is added to a note in the editor, once there is a note.
PlainTextField(
value = title,
onValueChange = { title = it },
@@ -115,19 +103,14 @@ fun ComposeSheet(
value = content,
onValueChange = { content = it },
modifier = Modifier.focusRequester(contentFocus),
- hint =
- if (kind == DraftKind.LIST) {
- R.string.compose_list_hint
- } else {
- R.string.compose_body_hint
- },
+ hint = R.string.compose_body_hint,
minLines = MIN_CONTENT_LINES,
)
SheetActions(
canSave = !saving && written,
onDiscard = onDismiss,
- onSave = { onSave(kind, title, content) },
+ onSave = { onSave(title, content) },
)
}
}
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt
index c500b84..d7999f7 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt
@@ -31,13 +31,13 @@ sealed interface EditorAction {
) : EditorAction
/**
- * Note ⇄ checklist.
+ * Give this note a checklist.
*
- * Only `kind` changes: the body text and any existing items both stay where
- * they are, so switching back and forth is lossless and a mis-tap costs
- * nothing.
+ * Not a conversion — a note HAS a checklist rather than BEING one (M13 step 2),
+ * so nothing moves and nothing is swapped: the body stays exactly where it is and
+ * the note gains a first, empty item for someone to type into.
*/
- data object ToggleKind : EditorAction
+ data object AddChecklist : EditorAction
data class SetPinned(
val pinned: Boolean,
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt
index 4fbb146..6f6c557 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt
@@ -84,15 +84,16 @@ fun EditorBottomBar(
contentDescription = stringResource(R.string.editor_reminder),
)
}
- IconButton(onClick = { onAction(EditorAction.ToggleKind) }) {
- val list = note.kind == KIND_LIST
- Icon(
- if (list) Icons.Filled.Create else Icons.AutoMirrored.Filled.List,
- contentDescription =
- stringResource(
- if (list) R.string.editor_make_note else R.string.editor_make_list,
- ),
- )
+ // Adds the first checklist item, which is what makes the checklist
+ // editor appear. Hidden once the note already has one — there is nothing
+ // left to add that the checklist's own "+" row doesn't do better.
+ if (note.items.isEmpty()) {
+ IconButton(onClick = { onAction(EditorAction.AddChecklist) }) {
+ Icon(
+ Icons.AutoMirrored.Filled.List,
+ contentDescription = stringResource(R.string.editor_add_checklist),
+ )
+ }
}
}
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt
index 91f290d..4f4af64 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt
@@ -64,9 +64,8 @@ fun NoteCard(
Spacer(Modifier.height(4.dp))
}
- if (note.kind == KIND_LIST) {
- Checklist(items = note.items)
- } else if (note.body.isNotBlank()) {
+ // Both, in order — a note can carry a body AND a checklist (M13 step 2).
+ if (note.body.isNotBlank()) {
Text(
text = note.body,
style = MaterialTheme.typography.bodyMedium,
@@ -74,6 +73,10 @@ fun NoteCard(
overflow = TextOverflow.Ellipsis,
)
}
+ if (note.items.isNotEmpty()) {
+ if (note.body.isNotBlank()) Spacer(Modifier.height(4.dp))
+ Checklist(items = note.items)
+ }
// A note with no title, no body and no items still has to occupy the
// board legibly — otherwise it reads as a rendering bug.
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt
index 9281fda..87e1bad 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt
@@ -150,16 +150,18 @@ fun NoteEditorScreen(
bold = true,
)
- if (note.kind == KIND_LIST) {
+ EditorField(
+ value = body,
+ onValueChange = { body = it },
+ hint = R.string.editor_body_hint,
+ enabled = !readOnly,
+ minLines = MIN_BODY_LINES,
+ )
+
+ // Below the body, not instead of it, and only once the note has items —
+ // the toolbar's add-checklist action is what puts the first one there.
+ if (note.items.isNotEmpty()) {
ChecklistEditor(note = note, readOnly = readOnly, onAction = onAction)
- } else {
- EditorField(
- value = body,
- onValueChange = { body = it },
- hint = R.string.editor_body_hint,
- enabled = !readOnly,
- minLines = MIN_BODY_LINES,
- )
}
if (note.labels.isNotEmpty()) {
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteKind.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteKind.kt
deleted file mode 100644
index 4f20da4..0000000
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteKind.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.fabledsword.thoughtsync.ui
-
-/**
- * The core's `kind` vocabulary, which the UI has to match exactly.
- *
- * Shared rather than repeated because it was already living in three places — the
- * card deciding whether to draw checkboxes, the editor deciding which field to
- * show, and the view model deciding what to create — and a typo in any one of them
- * would silently render a checklist as a paragraph rather than fail.
- *
- * Strings and not an enum: this is a value the STORE owns, arriving from a server
- * that may be newer than this client, and an unrecognised kind has to fall through
- * to "render it as a note" rather than throw.
- */
-internal const val KIND_TEXT = "text"
-internal const val KIND_LIST = "list"
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 04e5f28..9978d83 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -10,11 +10,8 @@
New note
- Note
- ListTitleTake a note…
- One item per lineDiscardSave
@@ -42,13 +39,12 @@
Open noteBack to notesTitle
+ Add a checklistNoteAdd itemRemove itemRemove labelSet a reminder
- Make a checklist
- Switch to a noteMore actionsPinUnpin
diff --git a/android/ffi/src/lib.rs b/android/ffi/src/lib.rs
index 190642b..c88a2c8 100644
--- a/android/ffi/src/lib.rs
+++ b/android/ffi/src/lib.rs
@@ -573,7 +573,6 @@ mod tests {
title: title.to_string(),
body: body.to_string(),
color: "default".to_string(),
- kind: None,
items: None,
}
}
@@ -677,7 +676,6 @@ mod tests {
title: "Packing".to_string(),
body: String::new(),
color: "default".to_string(),
- kind: Some("list".to_string()),
items: Some(vec!["socks".to_string()]),
})
.expect("create");
diff --git a/android/ffi/src/models.rs b/android/ffi/src/models.rs
index de42191..a202abd 100644
--- a/android/ffi/src/models.rs
+++ b/android/ffi/src/models.rs
@@ -35,7 +35,6 @@ pub struct Note {
pub display_title: String,
pub body: String,
pub color: String,
- pub kind: String,
pub position: i64,
pub pinned: bool,
pub archived: bool,
@@ -134,7 +133,6 @@ impl From for Note {
display_title,
body,
color,
- kind,
position,
pinned,
archived,
@@ -155,7 +153,6 @@ impl From for Note {
display_title,
body,
color,
- kind,
position,
pinned,
archived,
@@ -294,7 +291,6 @@ pub struct NoteQuery {
pub struct NoteFacets {
pub q: Option,
pub color: Option,
- pub kind: Option,
pub label: Option>,
pub has_reminder: Option,
pub has_attachment: Option,
@@ -324,7 +320,6 @@ impl From for core_models::Facets {
let NoteFacets {
q,
color,
- kind,
label,
has_reminder,
has_attachment,
@@ -334,7 +329,6 @@ impl From for core_models::Facets {
core_models::Facets {
q,
color,
- kind,
label,
has_reminder,
has_attachment,
@@ -351,8 +345,8 @@ pub struct NoteDraft {
pub body: String,
/// "default" unless the user picked a colour.
pub color: String,
- pub kind: Option,
- /// Checklist lines, for `kind = "checklist"`.
+ /// Checklist lines. A note can carry both a body and items (M13 step 2), so this
+ /// is not an alternative to `body` — it is an addition to it.
pub items: Option>,
}
@@ -362,14 +356,12 @@ impl From for core_models::NoteCreateInput {
title,
body,
color,
- kind,
items,
} = value;
core_models::NoteCreateInput {
title,
body,
color,
- kind,
items,
}
}
@@ -389,7 +381,6 @@ pub enum NoteEdit {
ClearTitle,
Body { value: String },
Color { value: String },
- Kind { value: String },
Pinned { value: bool },
Archived { value: bool },
RemindAt { value: String },
@@ -412,7 +403,6 @@ impl NoteEdit {
NoteEdit::ClearTitle => ("title", Value::Null),
NoteEdit::Body { value } => ("body", Value::String(value)),
NoteEdit::Color { value } => ("color", Value::String(value)),
- NoteEdit::Kind { value } => ("kind", Value::String(value)),
NoteEdit::Pinned { value } => ("pinned", Value::Bool(value)),
NoteEdit::Archived { value } => ("archived", Value::Bool(value)),
NoteEdit::RemindAt { value } => ("remind_at", Value::String(value)),
diff --git a/core/src/local/models.rs b/core/src/local/models.rs
index fa22918..33cfc47 100644
--- a/core/src/local/models.rs
+++ b/core/src/local/models.rs
@@ -14,7 +14,6 @@ pub struct Note {
pub display_title: String,
pub body: String,
pub color: String,
- pub kind: String,
pub position: i64,
pub pinned: bool,
pub archived: bool,
@@ -136,8 +135,6 @@ pub struct NoteCreateInput {
#[serde(default = "default_color")]
pub color: String,
#[serde(default)]
- pub kind: Option,
- #[serde(default)]
pub items: Option>,
}
@@ -162,8 +159,6 @@ pub struct Facets {
#[serde(default)]
pub color: Option,
#[serde(default)]
- pub kind: Option,
- #[serde(default)]
pub label: Option>,
#[serde(default)]
pub has_reminder: Option,
diff --git a/core/src/local/schema.rs b/core/src/local/schema.rs
index 5927324..dbc3af8 100644
--- a/core/src/local/schema.rs
+++ b/core/src/local/schema.rs
@@ -14,7 +14,7 @@ CREATE TABLE notes (
title TEXT,
body TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT 'default',
- kind TEXT NOT NULL DEFAULT 'text', -- 'text' | 'list'
+ kind TEXT NOT NULL DEFAULT 'text', -- dropped in v6; kept so DROP COLUMN has something to drop
position INTEGER NOT NULL DEFAULT 0,
pinned INTEGER NOT NULL DEFAULT 0,
archived INTEGER NOT NULL DEFAULT 0,
@@ -160,6 +160,16 @@ CREATE TABLE prefs (
);
"#;
+// v6 (M13 step 2): `kind` is gone. A checklist is something a note HAS, not something
+// a note IS — the column was a mode flag with no enum and no constraint behind it,
+// and `note_items` was never tied to it. Dropping it loses nothing: a note that was
+// 'list' keeps every one of its items.
+//
+// SQLite has supported DROP COLUMN since 3.35 (2021); rusqlite bundles well past it.
+const SCHEMA_V6: &str = r#"
+ALTER TABLE notes DROP COLUMN kind;
+"#;
+
/// Bring the database up to the latest schema. Idempotent.
pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
@@ -184,5 +194,9 @@ pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
conn.execute_batch(SCHEMA_V5)?;
conn.execute_batch("PRAGMA user_version = 5;")?;
}
+ if version < 6 {
+ conn.execute_batch(SCHEMA_V6)?;
+ conn.execute_batch("PRAGMA user_version = 6;")?;
+ }
Ok(())
}
diff --git a/core/src/local/store.rs b/core/src/local/store.rs
index d3fa9a5..07bef6b 100644
--- a/core/src/local/store.rs
+++ b/core/src/local/store.rs
@@ -139,7 +139,7 @@ fn load_previews(conn: &Connection, note_id: &str) -> rusqlite::Result rusqlite::Result {
let mut note = conn.query_row(
- "SELECT id, title, body, color, kind, position, pinned, archived, trashed, remind_at, recurrence, created_at, updated_at, trashed_at
+ "SELECT id, title, body, color, position, pinned, archived, trashed, remind_at, recurrence, created_at, updated_at, trashed_at
FROM notes WHERE id = ?1",
[id],
|r| {
@@ -152,20 +152,19 @@ fn load_note(conn: &Connection, id: &str) -> rusqlite::Result {
display_title: dt,
body,
color: r.get(3)?,
- kind: r.get(4)?,
- position: r.get(5)?,
- pinned: r.get(6)?,
- archived: r.get(7)?,
- trashed: r.get(8)?,
- deleted_at: r.get(13)?,
- remind_at: r.get(9)?,
- recurrence: r.get(10)?,
+ position: r.get(4)?,
+ pinned: r.get(5)?,
+ archived: r.get(6)?,
+ trashed: r.get(7)?,
+ deleted_at: r.get(12)?,
+ remind_at: r.get(8)?,
+ recurrence: r.get(9)?,
labels: Vec::new(),
items: Vec::new(),
attachments: Vec::new(),
previews: Vec::new(),
- created_at: r.get(11)?,
- updated_at: r.get(12)?,
+ created_at: r.get(10)?,
+ updated_at: r.get(11)?,
})
},
)?;
@@ -277,10 +276,6 @@ pub fn list_notes(conn: &Connection, q: &ListQuery) -> rusqlite::Result rusqlite::Resu
let id = new_id();
let ts = now();
let title = normalize_title(&input.title);
- let kind = input.kind.clone().unwrap_or_else(|| "text".to_string());
let position: i64 = conn.query_row(
"SELECT COALESCE(MAX(position), 0) + 1 FROM notes",
[],
|r| r.get(0),
)?;
conn.execute(
- "INSERT INTO notes (id, title, body, color, kind, position, created_at, updated_at, dirty)
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7, 1)",
- params![id, title, input.body, input.color, kind, position, ts],
+ "INSERT INTO notes (id, title, body, color, position, created_at, updated_at, dirty)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6, 1)",
+ params![id, title, input.body, input.color, position, ts],
)?;
if let Some(items) = &input.items {
for (i, text) in items.iter().enumerate() {
@@ -411,11 +405,6 @@ pub fn update_note(conn: &Connection, id: &str, changes: &Value) -> rusqlite::Re
conn.execute("UPDATE notes SET color = ?1 WHERE id = ?2", params![s, id])?;
}
}
- "kind" => {
- if let Some(s) = v.as_str() {
- conn.execute("UPDATE notes SET kind = ?1 WHERE id = ?2", params![s, id])?;
- }
- }
"pinned" => {
if let Some(b) = v.as_bool() {
conn.execute("UPDATE notes SET pinned = ?1 WHERE id = ?2", params![b, id])?;
diff --git a/core/src/sync/compat.rs b/core/src/sync/compat.rs
index e9a5908..8593448 100644
--- a/core/src/sync/compat.rs
+++ b/core/src/sync/compat.rs
@@ -19,11 +19,11 @@
use serde::{Deserialize, Serialize};
/// The sync wire protocol this client speaks.
-pub const CLIENT_PROTOCOL_VERSION: u32 = 1;
+pub const CLIENT_PROTOCOL_VERSION: u32 = 2;
/// The oldest server protocol this client can drive — the symmetric half of the
/// server's `min_client_protocol_version`.
-pub const MIN_SERVER_PROTOCOL_VERSION: u32 = 1;
+pub const MIN_SERVER_PROTOCOL_VERSION: u32 = 2;
/// Capabilities without which syncing is meaningless, so their absence BLOCKS the
/// link rather than degrading it.
diff --git a/core/src/sync/pull.rs b/core/src/sync/pull.rs
index 3b33c85..b1bff40 100644
--- a/core/src/sync/pull.rs
+++ b/core/src/sync/pull.rs
@@ -240,15 +240,14 @@ fn upsert_note(conn: &Connection, note: &wire::Note) -> rusqlite::Result<()> {
// `created_at` is deliberately absent from the UPDATE clause: a note's birth time
// never changes, and the server's copy is the same value anyway.
conn.execute(
- "INSERT INTO notes (id, title, body, color, kind, position, pinned, archived,
+ "INSERT INTO notes (id, title, body, color, position, pinned, archived,
trashed, remind_at, recurrence, created_at, updated_at,
sync_revision, trashed_at, dirty)
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, 0)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, 0)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
body = excluded.body,
color = excluded.color,
- kind = excluded.kind,
position = excluded.position,
pinned = excluded.pinned,
archived = excluded.archived,
@@ -264,7 +263,6 @@ fn upsert_note(conn: &Connection, note: &wire::Note) -> rusqlite::Result<()> {
note.title,
note.body,
note.color,
- note.kind,
note.position,
note.pinned,
note.archived,
@@ -501,7 +499,6 @@ mod tests {
title: Some("Title".into()),
body: "Body".into(),
color: "default".into(),
- kind: "text".into(),
position: 0,
pinned: false,
archived: false,
diff --git a/core/src/sync/push.rs b/core/src/sync/push.rs
index bb2e563..f4ca47b 100644
--- a/core/src/sync/push.rs
+++ b/core/src/sync/push.rs
@@ -69,7 +69,6 @@ pub struct Change {
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option,
#[serde(skip_serializing_if = "Option::is_none")]
- pub kind: Option,
#[serde(skip_serializing_if = "Option::is_none")]
pub pinned: Option,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -102,7 +101,6 @@ impl Change {
title: None,
body: None,
color: None,
- kind: None,
pinned: None,
archived: None,
trashed: None,
@@ -202,7 +200,6 @@ fn collect_labels(conn: &Connection, out: &mut Vec, limit: usize) -> rus
edited_at: r.get(3)?,
title: None,
body: None,
- kind: None,
pinned: None,
archived: None,
trashed: None,
@@ -240,7 +237,6 @@ struct NoteRow {
title: Option,
body: String,
color: String,
- kind: String,
position: i64,
pinned: bool,
archived: bool,
@@ -253,7 +249,7 @@ struct NoteRow {
fn note_row(conn: &Connection, id: &str) -> rusqlite::Result {
conn.query_row(
- "SELECT title, body, color, kind, position, pinned, archived, trashed,
+ "SELECT title, body, color, position, pinned, archived, trashed,
remind_at, recurrence, created_at, updated_at
FROM notes WHERE id = ?1",
params![id],
@@ -262,15 +258,14 @@ fn note_row(conn: &Connection, id: &str) -> rusqlite::Result {
title: r.get(0)?,
body: r.get(1)?,
color: r.get(2)?,
- kind: r.get(3)?,
- position: r.get(4)?,
- pinned: r.get::<_, i64>(5)? != 0,
- archived: r.get::<_, i64>(6)? != 0,
- trashed: r.get::<_, i64>(7)? != 0,
- remind_at: r.get(8)?,
- recurrence: r.get(9)?,
- created_at: r.get(10)?,
- updated_at: r.get(11)?,
+ position: r.get(3)?,
+ pinned: r.get::<_, i64>(4)? != 0,
+ archived: r.get::<_, i64>(5)? != 0,
+ trashed: r.get::<_, i64>(6)? != 0,
+ remind_at: r.get(7)?,
+ recurrence: r.get(8)?,
+ created_at: r.get(9)?,
+ updated_at: r.get(10)?,
})
},
)
@@ -312,7 +307,6 @@ fn note_change(conn: &Connection, id: &str) -> rusqlite::Result {
title: row.title,
body: Some(row.body),
color: Some(row.color),
- kind: Some(row.kind),
pinned: Some(row.pinned),
archived: Some(row.archived),
trashed: Some(row.trashed),
@@ -535,9 +529,9 @@ mod tests {
fn seed_note(conn: &Connection, id: &str, dirty: i64) {
conn.execute(
- "INSERT INTO notes (id, title, body, color, kind, position, pinned, archived,
+ "INSERT INTO notes (id, title, body, color, position, pinned, archived,
trashed, created_at, updated_at, sync_revision, dirty)
- VALUES (?1, 'T', 'B', 'default', 'text', 0, 0, 0, 0,
+ VALUES (?1, 'T', 'B', 'default', 0, 0, 0, 0,
'2026-07-26T00:00:00.000Z', '2026-07-26T00:00:00.000Z', 3, ?2)",
params![id, dirty],
)
diff --git a/core/src/sync/wire.rs b/core/src/sync/wire.rs
index 92aedef..8a80dbb 100644
--- a/core/src/sync/wire.rs
+++ b/core/src/sync/wire.rs
@@ -29,8 +29,6 @@ pub struct Note {
pub body: String,
#[serde(default = "default_color")]
pub color: String,
- #[serde(default = "default_kind")]
- pub kind: String,
#[serde(default)]
pub position: i64,
#[serde(default)]
@@ -157,10 +155,6 @@ fn default_color() -> String {
"default".to_string()
}
-fn default_kind() -> String {
- "text".to_string()
-}
-
fn default_mime() -> String {
"application/octet-stream".to_string()
}
diff --git a/frontend/src/adapters/repo.ts b/frontend/src/adapters/repo.ts
index 0b319e0..2efff6e 100644
--- a/frontend/src/adapters/repo.ts
+++ b/frontend/src/adapters/repo.ts
@@ -10,7 +10,7 @@
// stays in the stores — the repo is data access only.
import type { NoteColor } from "../notes/colors";
-import type { Note, NoteFacets, NoteView, NoteKind, NoteRevision } from "../stores/notes";
+import type { Note, NoteFacets, NoteView, NoteRevision } from "../stores/notes";
import type { Label } from "../stores/labels";
import type { SavedFilter } from "../stores/savedFilters";
import type { Device } from "../stores/devices";
@@ -35,13 +35,12 @@ export interface NoteCreateInput {
title: string;
body: string;
color: NoteColor;
- kind?: NoteKind;
items?: string[];
}
// The mutable subset of a note (PATCH /api/notes/:id).
export type NoteChanges = Partial<
- Pick
+ Pick
>;
export interface ChecklistItemChanges {
diff --git a/frontend/src/adapters/rest.ts b/frontend/src/adapters/rest.ts
index 9040431..4b161a5 100644
--- a/frontend/src/adapters/rest.ts
+++ b/frontend/src/adapters/rest.ts
@@ -32,7 +32,6 @@ function notesQuery(q: NoteListQuery): string {
for (const id of q.facets?.label ?? []) if (id) params.append("label", id);
if (q.facets?.q) params.set("q", q.facets.q);
if (q.facets?.color) params.set("color", q.facets.color);
- if (q.facets?.kind) params.set("kind", q.facets.kind);
if (q.facets?.has_reminder) params.set("has_reminder", "true");
if (q.facets?.has_attachment) params.set("has_attachment", "true");
if (q.facets?.created_after) params.set("created_after", q.facets.created_after);
diff --git a/frontend/src/components/FilterBar.vue b/frontend/src/components/FilterBar.vue
index e04406a..d448977 100644
--- a/frontend/src/components/FilterBar.vue
+++ b/frontend/src/components/FilterBar.vue
@@ -11,7 +11,7 @@ import { NOTE_COLOR_KEYS, NOTE_COLOR_LABELS, NOTE_SWATCH_CLASSES, type NoteColor
import Icon from "./Icon.vue";
// A dead-simple facet bar over the board: text search + color + labels + has-reminder
-// + has-attachment + kind + created-date range. The URL query IS the state, so a
+// + has-attachment + created-date range. The URL query IS the state, so a
// filtered board is a shareable lens and a saved view is just a link.
const route = useRoute();
const router = useRouter();
@@ -35,9 +35,6 @@ function clearAll() {
function setColor(c: NoteColor) {
patch({ color: facets.value.color === c ? undefined : c });
}
-function setKind(k: "text" | "list") {
- patch({ kind: facets.value.kind === k ? undefined : k });
-}
function toggleLabel(id: string) {
const cur = facets.value.label ?? [];
const next = cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id];
@@ -164,12 +161,6 @@ const chipOff = "border-neutral-300 text-neutral-600 hover:bg-neutral-100 dark:b
-
-