diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 2011bfb..dc4d5a8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -197,10 +197,21 @@ fun CalendarHost( // 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) } + // Where a restore came from, so closing the import puts it back: the import + // overlays are declared under Backup & restore and the manager, so starting + // one has to close them — without this, finishing a restore drops you on the + // calendar and the next file means walking in through Settings again. + var backupAfterImport by rememberSaveable { mutableStateOf(false) } + var calendarsAfterImport by rememberSaveable { mutableStateOf(false) } + // One import run. The import VM lives in the Activity's store, so this is + // what tells it a *re-import of the same file* is new work and not the run + // it already finished; a rotation keeps the number and keeps the result. + var importSession by rememberSaveable { mutableStateOf(0) } LaunchedEffect(requestedImportUri) { if (requestedImportUri != null) { importUri = requestedImportUri importForceMany = false + importSession++ onImportConsumed() } } @@ -510,11 +521,28 @@ fun CalendarHost( importUri?.let { uri -> ImportScreen( uri = uri, + session = importSession, forceMany = importForceMany, - onClose = { importUri = null }, - onManageCalendars = { showCalendars = true }, + onClose = { + importUri = null + // Back to the surface the restore started from, ready for + // the next file. + showCalendars = calendarsAfterImport + showBackup = backupAfterImport + calendarsAfterImport = false + backupAfterImport = false + }, + onManageCalendars = { + // The manager is where this leads, so it must not be + // reopened underneath on close. + calendarsAfterImport = false + backupAfterImport = false + showCalendars = true + }, onOpenSingle = { form -> importUri = null + calendarsAfterImport = false + backupAfterImport = false importFormSource = ImportSource.File importForm = form }, @@ -569,6 +597,9 @@ fun CalendarHost( onImport = { importUri = it importForceMany = true + importSession++ + backupAfterImport = true + calendarsAfterImport = showCalendars showBackup = false showCalendars = false }, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt index a3b7690..fcf33d3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt @@ -73,19 +73,19 @@ import de.jeanlucmakiola.floret.identity.predictiveBack @Composable fun ImportScreen( uri: Uri, + session: Int, onClose: () -> Unit, onOpenSingle: (EventForm) -> Unit, forceMany: Boolean = false, onManageCalendars: (() -> Unit)? = null, - // Key the VM by the file uri. This screen has no nav backstack, so an - // unkeyed hiltViewModel() resolves to the Activity's store and is retained - // across imports — its one-shot `load` guard would then show the *previous* - // file's parsed state on the next import (a second restore, export→restore, - // etc.). Keying per uri hands each distinct file a fresh VM (fresh Loading - // state), while the same uri (rotation) reuses it and holds the result. - viewModel: ImportViewModel = hiltViewModel(key = uri.toString()), + viewModel: ImportViewModel = hiltViewModel(), ) { - LaunchedEffect(uri) { viewModel.load(uri, forceMany) } + // hiltViewModel() resolves to the Activity's store and is retained across + // imports, so the reload is driven by [session] rather than by a fresh VM: + // keying per uri looked right but handed a *re-import of the same file* the + // previous run's finished state, importing nothing (#304). A rotation keeps + // the session and so keeps the result. + LaunchedEffect(session) { viewModel.load(uri, forceMany, session) } val state by viewModel.state.collectAsStateWithLifecycle() // A single event isn't shown here — it opens the create form for review. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportViewModel.kt index 46a18a2..8dceb1f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportViewModel.kt @@ -16,6 +16,7 @@ import de.jeanlucmakiola.calendula.domain.ics.ParsedIcsEvent import de.jeanlucmakiola.calendula.domain.ics.toEventForm import de.jeanlucmakiola.calendula.domain.isEventTarget import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -69,18 +70,26 @@ class ImportViewModel @Inject constructor( private val parser = IcsParser() private val _state = MutableStateFlow(ImportUiState.Loading) val state: StateFlow = _state.asStateFlow() - private var started = false + private var loadedSession: Int? = null + private var loadJob: Job? = null /** - * Read + parse [uri] once; subsequent calls (recomposition) are ignored. + * Read + parse [uri] once per [session]; recomposition (and a rotation, + * which keeps the session) re-calls this and keeps the result. A new import + * of the *same* file is a new session, and has to parse and run again — + * keying on the uri alone showed the previous run's summary and imported + * nothing (#304). + * * 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 - started = true - viewModelScope.launch { + fun load(uri: Uri, forceMany: Boolean = false, session: Int = 0) { + if (loadedSession == session) return + loadedSession = session + loadJob?.cancel() + _state.value = ImportUiState.Loading + loadJob = viewModelScope.launch { val parsed = withContext(io) { importer.readText(uri)?.let(parser::parse) }