feat: in-app restore always uses the full import flow
Route by entry point, not just event count. Opening a .ics from outside still sends a single event straight to the prefilled create form (add one event, e.g. a ticket). The in-app 'Restore from .ics' button passes forceMany so even a single-event backup goes through the calendar picker + summary — its intent is 'restore a backup', not 'add this event'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -172,9 +172,15 @@ fun CalendarHost(
|
|||||||
// picker (many). A plain conditional overlay (no slide) — it's transient.
|
// picker (many). A plain conditional overlay (no slide) — it's transient.
|
||||||
var importUri by remember { mutableStateOf<android.net.Uri?>(null) }
|
var importUri by remember { mutableStateOf<android.net.Uri?>(null) }
|
||||||
var importForm by remember { mutableStateOf<EventForm?>(null) }
|
var importForm by remember { mutableStateOf<EventForm?>(null) }
|
||||||
|
// A restore (in-app "Restore from .ics" button) always runs the full import
|
||||||
|
// flow — picker + summary — even for a single-event file, because the intent
|
||||||
|
// is "restore a backup", not "add this one event". An externally opened .ics
|
||||||
|
// keeps routing a single event straight into the prefilled create form.
|
||||||
|
var importForceMany by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(requestedImportUri) {
|
LaunchedEffect(requestedImportUri) {
|
||||||
if (requestedImportUri != null) {
|
if (requestedImportUri != null) {
|
||||||
importUri = requestedImportUri
|
importUri = requestedImportUri
|
||||||
|
importForceMany = false
|
||||||
onImportConsumed()
|
onImportConsumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,7 +420,7 @@ fun CalendarHost(
|
|||||||
) {
|
) {
|
||||||
CalendarsScreen(
|
CalendarsScreen(
|
||||||
onBack = { showCalendars = false },
|
onBack = { showCalendars = false },
|
||||||
onImport = { importUri = it },
|
onImport = { importUri = it; importForceMany = true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,6 +429,7 @@ fun CalendarHost(
|
|||||||
importUri?.let { uri ->
|
importUri?.let { uri ->
|
||||||
ImportScreen(
|
ImportScreen(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
forceMany = importForceMany,
|
||||||
onClose = { importUri = null },
|
onClose = { importUri = null },
|
||||||
onOpenSingle = { form ->
|
onOpenSingle = { form ->
|
||||||
importUri = null
|
importUri = null
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
|||||||
* Handles an opened/received `.ics` file. A single event is handed straight to
|
* Handles an opened/received `.ics` file. A single event is handed straight to
|
||||||
* the prefilled create form via [onOpenSingle]; several events show a target-
|
* the prefilled create form via [onOpenSingle]; several events show a target-
|
||||||
* calendar picker and import in bulk (dedup by UID), then a result summary.
|
* calendar picker and import in bulk (dedup by UID), then a result summary.
|
||||||
* Empty/failed files show a short message and close.
|
* Empty/failed files show a short message and close. [forceMany] keeps a
|
||||||
|
* single-event file on the bulk path — used by the in-app restore, whose intent
|
||||||
|
* is "restore a backup" rather than "add this one event".
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -72,9 +74,10 @@ fun ImportScreen(
|
|||||||
uri: Uri,
|
uri: Uri,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onOpenSingle: (EventForm) -> Unit,
|
onOpenSingle: (EventForm) -> Unit,
|
||||||
|
forceMany: Boolean = false,
|
||||||
viewModel: ImportViewModel = hiltViewModel(),
|
viewModel: ImportViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(uri) { viewModel.load(uri) }
|
LaunchedEffect(uri) { viewModel.load(uri, forceMany) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// A single event isn't shown here — it opens the create form for review.
|
// A single event isn't shown here — it opens the create form for review.
|
||||||
|
|||||||
@@ -66,8 +66,13 @@ class ImportViewModel @Inject constructor(
|
|||||||
val state: StateFlow<ImportUiState> = _state.asStateFlow()
|
val state: StateFlow<ImportUiState> = _state.asStateFlow()
|
||||||
private var started = false
|
private var started = false
|
||||||
|
|
||||||
/** Read + parse [uri] once; subsequent calls (recomposition) are ignored. */
|
/**
|
||||||
fun load(uri: Uri) {
|
* Read + parse [uri] once; subsequent calls (recomposition) are ignored.
|
||||||
|
* When [forceMany] is set (an in-app restore), a single-event file still goes
|
||||||
|
* through the bulk picker + summary rather than the prefilled create form —
|
||||||
|
* a restore is "bring back a backup", not "add this one event".
|
||||||
|
*/
|
||||||
|
fun load(uri: Uri, forceMany: Boolean = false) {
|
||||||
if (started) return
|
if (started) return
|
||||||
started = true
|
started = true
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -77,7 +82,7 @@ class ImportViewModel @Inject constructor(
|
|||||||
_state.value = when {
|
_state.value = when {
|
||||||
parsed == null -> ImportUiState.Failed
|
parsed == null -> ImportUiState.Failed
|
||||||
parsed.events.isEmpty() -> ImportUiState.Empty
|
parsed.events.isEmpty() -> ImportUiState.Empty
|
||||||
parsed.events.size == 1 -> ImportUiState.Single(
|
parsed.events.size == 1 && !forceMany -> ImportUiState.Single(
|
||||||
form = parsed.events.single().toEventForm(TimeZone.currentSystemDefault()),
|
form = parsed.events.single().toEventForm(TimeZone.currentSystemDefault()),
|
||||||
warnings = parsed.warnings,
|
warnings = parsed.warnings,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user