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 e0ce981..25cda63 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -412,7 +412,10 @@ fun CalendarHost( enter = slideInHorizontally(slideSpec) { it } + fadeIn(), exit = slideOutHorizontally(slideSpec) { it } + fadeOut(), ) { - CalendarsScreen(onBack = { showCalendars = false }) + CalendarsScreen( + onBack = { showCalendars = false }, + onImport = { importUri = it }, + ) } // Import flow for an opened/received .ics file. A single event routes diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt index 9d087c6..3d82152 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.FileDownload +import androidx.compose.material.icons.filled.FileUpload import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.PhoneAndroid @@ -112,6 +113,16 @@ import java.time.LocalDate /** Sentinel [editorId] meaning "the editor is composing a new calendar". */ private const val NEW_CALENDAR_ID = Long.MIN_VALUE +// SAF mime filter for the restore picker. `.ics` files reach us under several +// mimes depending on the source app (our own export uses text/calendar; others +// hand them out as octet-stream or text/plain), so accept the common set rather +// than hide valid backups behind an over-tight filter. +private val RESTORE_MIME_TYPES = arrayOf( + "text/calendar", + "application/octet-stream", + "text/plain", +) + /** * Calendar manager (reached from Settings). Lists the app's own device-only * calendars with create / rename / recolor / delete (via a full-screen editor), @@ -122,6 +133,7 @@ private const val NEW_CALENDAR_ID = Long.MIN_VALUE @Composable fun CalendarsScreen( onBack: () -> Unit, + onImport: (android.net.Uri) -> Unit, viewModel: CalendarsViewModel = hiltViewModel(), ) { val calendars by viewModel.calendars.collectAsStateWithLifecycle() @@ -168,6 +180,7 @@ fun CalendarsScreen( onConsumeError = viewModel::consumeError, backupResult = backupResult, onExportBackup = viewModel::exportBackup, + onImport = onImport, onConsumeBackupResult = viewModel::consumeBackupResult, autoBackup = autoBackup, onSetAutoBackupEnabled = viewModel::setAutoBackupEnabled, @@ -191,6 +204,7 @@ private fun CalendarsList( onConsumeError: () -> Unit, backupResult: BackupResult?, onExportBackup: (android.net.Uri) -> Unit, + onImport: (android.net.Uri) -> Unit, onConsumeBackupResult: () -> Unit, autoBackup: AutoBackupUiState, onSetAutoBackupEnabled: (Boolean) -> Unit, @@ -223,6 +237,13 @@ private fun CalendarsList( contract = ActivityResultContracts.CreateDocument("text/calendar"), ) { uri -> uri?.let(onExportBackup) } + // SAF "open document" picker for restoring events from a .ics file. The + // picked Uri is handed up to the host, which runs it through the same import + // flow as an externally opened .ics (parse, dedup by UID, target picker). + val openBackup = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + ) { uri -> uri?.let(onImport) } + // SAF folder picker for the automatic-backup destination; the VM persists the // write grant so background runs can keep writing to it. val pickFolder = rememberLauncherForActivityResult( @@ -316,6 +337,15 @@ private fun CalendarsList( runCatching { createBackup.launch("calendula-backup-${LocalDate.now()}.ics") } }, ) + GroupedRow( + title = stringResource(R.string.calendars_restore_action), + summary = stringResource(R.string.calendars_restore_hint), + position = Position.Middle, + leading = { LeadingAvatar(Icons.Default.FileUpload) }, + onClick = { + runCatching { openBackup.launch(RESTORE_MIME_TYPES) } + }, + ) GroupedRow( title = stringResource(R.string.calendars_auto_backup), summary = stringResource(R.string.calendars_auto_backup_hint), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c9e8df..d3272e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -428,6 +428,8 @@ Backup Local calendars aren\'t synced anywhere, so export them to an .ics file to keep a copy. Export as .ics file + Restore from .ics file + Import events from a backup or another calendar app. Automatic backup Periodically export your local calendars to a folder as an .ics file. Backup folder