feat(watch): sync indicator UI + POST_NOTIFICATIONS prompt
CaseListScreen shows a small ☁↑N at the top whenever the worker has queue items; per-row ↑ next to the timestamp marks individual cases that are still mid-upload. Both surface the SyncStateProvider directly so the user sees the queue draining in real time. MainActivity prompts for POST_NOTIFICATIONS on Wear OS 13+ — the foreground service runs either way, but on API 33+ the persistent notification only renders if the runtime grant is given. Result is informational; we don't gate the recording flow on it (the doctor still needs to be able to dictate even after a 'deny').
This commit is contained in:
@@ -37,6 +37,7 @@ fun CaseListScreen(
|
||||
) {
|
||||
val app = LocalContext.current.applicationContext as DoctateApp
|
||||
val cases by app.caseStore.casesFlow.collectAsStateWithLifecycle()
|
||||
val sync by app.syncStateProvider.state.collectAsStateWithLifecycle()
|
||||
val listState = rememberScalingLazyListState()
|
||||
|
||||
// Display oldest-first so the newest entry sits next to the `Neu` EdgeButton.
|
||||
@@ -62,6 +63,17 @@ fun CaseListScreen(
|
||||
if (displayed.isEmpty()) {
|
||||
EmptyState(onNewCase)
|
||||
} else {
|
||||
if (sync.queueLen > 0) {
|
||||
Text(
|
||||
text = "☁↑${sync.queueLen}",
|
||||
style = MaterialTheme.typography.bodyExtraSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 2.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
ScalingLazyColumn(
|
||||
state = listState,
|
||||
contentPadding = contentPadding,
|
||||
@@ -70,7 +82,11 @@ fun CaseListScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(displayed, key = { it.caseId }) { case ->
|
||||
CaseRow(case, onTap = { onCaseTap(case.caseId) })
|
||||
CaseRow(
|
||||
case = case,
|
||||
pendingUpload = case.caseId in sync.pendingCaseIds,
|
||||
onTap = { onCaseTap(case.caseId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,14 +94,14 @@ fun CaseListScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CaseRow(case: CaseEntry, onTap: () -> Unit) {
|
||||
private fun CaseRow(case: CaseEntry, pendingUpload: Boolean, onTap: () -> Unit) {
|
||||
Card(
|
||||
onClick = onTap,
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = formatTime(case.createdAt),
|
||||
text = if (pendingUpload) "${formatTime(case.createdAt)} ↑" else formatTime(case.createdAt),
|
||||
style = MaterialTheme.typography.bodyExtraSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package com.doctate.watch.presentation
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val seq = AtomicLong(0)
|
||||
@@ -13,8 +18,14 @@ class MainActivity : ComponentActivity() {
|
||||
NavCommand.Pending(0L, NavCommand.ShowList),
|
||||
)
|
||||
|
||||
private val notificationsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
) { /* result is informational; the foreground service still runs without it,
|
||||
but the persistent notification will be hidden on Wear OS 13+. */ }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestNotificationPermissionIfNeeded()
|
||||
dispatch(intent)
|
||||
setContent { AppNav(pendingCommand = pendingCommand) }
|
||||
}
|
||||
@@ -31,4 +42,15 @@ class MainActivity : ComponentActivity() {
|
||||
cmd = NavCommand.fromIntent(intent),
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestNotificationPermissionIfNeeded() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||
val granted = ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (!granted) {
|
||||
notificationsLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user