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