diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f1c66d5..09953ca 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,25 @@
-->
+
+ android:theme="@style/Theme.ThoughtSync"
+ android:usesCleartextTraffic="true">
Screen.SYNC
+ editing != null -> Screen.EDITOR
+ else -> Screen.BOARD
+ }
+
+ when (screen) {
+ Screen.SYNC ->
+ SyncScreen(
+ state = sync.state,
+ onClose = { showingSync = false },
+ onProbe = sync::probe,
+ onClearProbe = sync::clearProbe,
+ onLink = sync::link,
+ onSyncNow = sync::syncNow,
+ onUnlink = sync::unlink,
+ onDismissRevokeNotice = sync::dismissRevokeNotice,
+ )
+
+ Screen.EDITOR ->
+ NoteEditorScreen(
+ // Non-null by construction: `screen` is EDITOR only when it is.
+ note = requireNotNull(editing) { "the editor screen needs a note" },
+ labels = board.state.labels,
+ saving = board.state.saving,
+ error = board.state.error,
+ // The one seam between the editor and the store. Exhaustive at the
+ // other end, so a new action cannot be added without being handled.
+ onAction = { board.onEditorAction(editing, it) },
+ )
+
+ Screen.BOARD -> {
+ BoardScreen(
+ state = board.state,
+ onOpen = board::open,
+ onOpenNote = board::openNote,
+ syncSummary = syncSummary(sync),
+ onOpenSync = { showingSync = true },
+ onSearch = board::search,
+ onCompose = { composing = true },
+ onDismissError = board::dismissError,
+ )
+
+ if (composing) {
+ ComposeSheet(
+ saving = board.state.saving,
+ onDismiss = { composing = false },
+ onSave = { kind, title, content ->
+ board.create(kind, title, content)
+ composing = false
+ },
+ )
+ }
+ }
}
- BoardScreen(
- state = model.state,
- onOpen = model::open,
- onOpenNote = model::openNote,
- onSearch = model::search,
- onCompose = { composing = true },
- onDismissError = model::dismissError,
- )
+ // The sync screen has no back handler of its own, so one lives here. The
+ // editor keeps its own, because it has to save the open note before leaving.
+ BackHandler(enabled = showingSync) { showingSync = false }
+}
- if (composing) {
- ComposeSheet(
- saving = model.state.saving,
- onDismiss = { composing = false },
- onSave = { kind, title, content ->
- model.create(kind, title, content)
- composing = false
- },
- )
+/**
+ * One line of sync state for the drawer, or null when there is nothing to say.
+ *
+ * Deliberately silent while the status is still loading and when the device is
+ * simply unlinked-and-idle — an "Off" badge on a local-first app would frame its
+ * normal resting state as something switched off.
+ */
+@Composable
+private fun syncSummary(sync: SyncViewModel): String? {
+ val state = sync.state
+ return when {
+ state.loading -> null
+ !state.linked -> null
+ state.pending -> stringResource(R.string.sync_badge_unsent)
+ else -> stringResource(R.string.sync_badge_on)
}
}
diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardScreen.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardScreen.kt
index 46db948..c490a6f 100644
--- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardScreen.kt
+++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardScreen.kt
@@ -56,6 +56,14 @@ fun BoardScreen(
state: BoardState,
onOpen: (Destination) -> Unit,
onOpenNote: (Note) -> Unit,
+ /**
+ * One line of sync state for the drawer, or null when there is nothing worth
+ * saying. Passed in rather than read from [BoardState]: sync has its own view
+ * model, and giving the board a copy of it would be two sources of truth for
+ * whether this device is linked.
+ */
+ syncSummary: String?,
+ onOpenSync: () -> Unit,
onSearch: (String) -> Unit,
onCompose: () -> Unit,
onDismissError: () -> Unit,
@@ -69,10 +77,15 @@ fun BoardScreen(
NavigationDrawer(
current = state.destination,
labels = state.labels,
+ syncSummary = syncSummary,
onOpen = {
onOpen(it)
scope.launch { drawerState.close() }
},
+ onOpenSync = {
+ onOpenSync()
+ scope.launch { drawerState.close() }
+ },
)
},
) {
@@ -172,7 +185,9 @@ private fun SearchBar(
private fun NavigationDrawer(
current: Destination,
labels: List