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 app = LocalContext.current.applicationContext as DoctateApp
|
||||||
val cases by app.caseStore.casesFlow.collectAsStateWithLifecycle()
|
val cases by app.caseStore.casesFlow.collectAsStateWithLifecycle()
|
||||||
|
val sync by app.syncStateProvider.state.collectAsStateWithLifecycle()
|
||||||
val listState = rememberScalingLazyListState()
|
val listState = rememberScalingLazyListState()
|
||||||
|
|
||||||
// Display oldest-first so the newest entry sits next to the `Neu` EdgeButton.
|
// Display oldest-first so the newest entry sits next to the `Neu` EdgeButton.
|
||||||
@@ -62,6 +63,17 @@ fun CaseListScreen(
|
|||||||
if (displayed.isEmpty()) {
|
if (displayed.isEmpty()) {
|
||||||
EmptyState(onNewCase)
|
EmptyState(onNewCase)
|
||||||
} else {
|
} 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(
|
ScalingLazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
@@ -70,7 +82,11 @@ fun CaseListScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
items(displayed, key = { it.caseId }) { case ->
|
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
|
@Composable
|
||||||
private fun CaseRow(case: CaseEntry, onTap: () -> Unit) {
|
private fun CaseRow(case: CaseEntry, pendingUpload: Boolean, onTap: () -> Unit) {
|
||||||
Card(
|
Card(
|
||||||
onClick = onTap,
|
onClick = onTap,
|
||||||
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
|
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp),
|
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = formatTime(case.createdAt),
|
text = if (pendingUpload) "${formatTime(case.createdAt)} ↑" else formatTime(case.createdAt),
|
||||||
style = MaterialTheme.typography.bodyExtraSmall,
|
style = MaterialTheme.typography.bodyExtraSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package com.doctate.watch.presentation
|
package com.doctate.watch.presentation
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
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 java.util.concurrent.atomic.AtomicLong
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val seq = AtomicLong(0)
|
private val seq = AtomicLong(0)
|
||||||
@@ -13,8 +18,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
NavCommand.Pending(0L, NavCommand.ShowList),
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
requestNotificationPermissionIfNeeded()
|
||||||
dispatch(intent)
|
dispatch(intent)
|
||||||
setContent { AppNav(pendingCommand = pendingCommand) }
|
setContent { AppNav(pendingCommand = pendingCommand) }
|
||||||
}
|
}
|
||||||
@@ -31,4 +42,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
cmd = NavCommand.fromIntent(intent),
|
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