Compare commits

..
Author SHA1 Message Date
makiolaj faa3e3817f Point floret-kit at main after #17 merged (#66) 2026-10-16 15:06:28 +02:00
makiolaj 71591f617a Bump floret-kit to latest main (#66) 2026-10-16 14:56:23 +02:00
makiolaj eea3a8b917 Address review on the Calendula alignment (#66)
Bumps floret-kit so a closing screen stops taking back, returns from
Storage to Import, and renames the per-list notifications row.
2026-10-16 14:42:47 +02:00
makiolaj a454c3685e Align motion and settings with Calendula (#66)
Screens slide in and leave through the predictive-back exit, settings
sub-screens get Calendula's back preview, Reminders becomes Notifications,
and export/import move out of Storage into Backup & restore. Bumps
floret-kit for the language picker header.
2026-10-16 14:27:15 +02:00
18 changed files with 222 additions and 144 deletions
+7
View File
@@ -16,6 +16,13 @@ All notable changes to this project are documented here. The format follows
those tasks, including their reminders and tags. They can tick off a single
occurrence of a recurring task. Task lists stay read-only for them (#63).
### Changed
- Screens now open and close the way they do in Calendula: they slide in, and
a back swipe previews the screen behind before closing (#66).
- Settings now use Calendula's names and layout. Reminders is now
Notifications, Task form is now New task form, and export and import moved
out of Storage into a new Backup & restore section (#66).
## [1.1.0] - 2026-10-10
### Added
+1 -1
View File
@@ -167,7 +167,7 @@ Agendula's own database go with it, so sync them to a CalDAV account first.
Each Codeberg release also carries `agendula-offline_v<version>.apk`: Agendula
without CalDAV sync and **without the network permission**, so it cannot
connect to anything. It installs alongside the regular app rather than replacing
it; move tasks between the two with Settings → Storage → Export / Import. In
it; move tasks between the two with Settings → Backup & restore → Export / Import. In
Obtainium, pick it with *Filter APKs by regular expression* set to
`^agendula-offline_v`.
@@ -70,6 +70,7 @@ internal fun AccountDetailScreen(
onSignInAgain: (AccountEntity) -> Unit,
onOpenStorage: () -> Unit,
viewModel: AccountsViewModel,
visible: Boolean,
) {
val accounts by viewModel.accounts.collectAsStateWithLifecycle()
val usable by viewModel.accountsUsable.collectAsStateWithLifecycle()
@@ -90,7 +91,7 @@ internal fun AccountDetailScreen(
// shown on the accounts list rather than here.
var confirming by remember { mutableStateOf(false) }
CollapsingScaffold(title = identity.title, onBack = onBack) {
CollapsingScaffold(title = identity.title, onBack = onBack, predictiveBack = true, visible = visible) {
AccountHero(identity)
Spacer(Modifier.height(24.dp))
@@ -48,6 +48,7 @@ internal fun AccountsScreen(
onOpenStorage: () -> Unit,
onBack: () -> Unit,
viewModel: AccountsViewModel,
visible: Boolean,
) {
val accounts by viewModel.accounts.collectAsStateWithLifecycle()
val usable by viewModel.accountsUsable.collectAsStateWithLifecycle()
@@ -60,6 +61,8 @@ internal fun AccountsScreen(
CollapsingScaffold(
title = stringResource(R.string.settings_section_accounts),
onBack = onBack,
predictiveBack = true,
visible = visible,
) {
val loaded = accounts ?: return@CollapsingScaffold
@@ -31,8 +31,8 @@ internal fun AccountSections(
// Which account the detail screen is showing; the section alone cannot say.
var openAccount by rememberSaveable(initialAccountId) { mutableStateOf(initialAccountId) }
// Accounts stays composed under Add account, for the same reason Storage
// stays composed under Export: the deeper screen slides over it.
// Accounts stays composed under Add account, for the same reason Backup
// stays composed under export and import: the deeper screen slides over it.
val accountsOpen = section == SettingsSection.Accounts ||
section?.parent == SettingsSection.Accounts
SlideInSection(visible = accountsOpen) {
@@ -50,9 +50,11 @@ internal fun AccountSections(
onOpenStorage = { onSection(SettingsSection.Storage) },
onBack = { onSection(null) },
viewModel = accountsViewModel,
visible = accountsOpen,
)
}
SlideInSection(visible = section == SettingsSection.Account) {
val accountOpen = section == SettingsSection.Account
SlideInSection(visible = accountOpen) {
openAccount?.let { id ->
AccountDetailScreen(
accountId = id,
@@ -61,6 +63,7 @@ internal fun AccountSections(
onSignInAgain = signInAgain,
onOpenStorage = { onSection(SettingsSection.Storage) },
viewModel = accountsViewModel,
visible = accountOpen,
)
}
}
+1
View File
@@ -50,6 +50,7 @@
<application
android:name=".AgendulaApp"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
@@ -70,6 +70,7 @@ private const val EXTERNAL_STORAGE = "com.android.externalstorage.documents"
@Composable
fun ExportScreen(
onBack: () -> Unit,
visible: Boolean,
modifier: Modifier = Modifier,
viewModel: ExportViewModel = hiltViewModel(),
) {
@@ -89,6 +90,8 @@ fun ExportScreen(
title = stringResource(R.string.settings_export),
onBack = onBack,
modifier = modifier,
predictiveBack = true,
visible = visible,
) {
Text(
text = stringResource(R.string.export_hint),
@@ -76,6 +76,8 @@ private val PICK_TYPES = arrayOf("text/calendar", "text/x-vcalendar", "applicati
fun ImportScreen(
onBack: () -> Unit,
modifier: Modifier = Modifier,
predictiveBack: Boolean = false,
visible: Boolean? = null,
initialUri: Uri? = null,
onOpenStorage: (() -> Unit)? = null,
viewModel: ImportViewModel = hiltViewModel(),
@@ -105,6 +107,8 @@ fun ImportScreen(
title = stringResource(R.string.import_title),
onBack = onBack,
modifier = modifier,
predictiveBack = predictiveBack,
visible = visible,
) {
Text(
text = stringResource(R.string.import_hint),
@@ -25,12 +25,14 @@ import de.jeanlucmakiola.floret.components.positionOf
* source, which is the specific thing §3.2(a) requires us to point at.
*/
@Composable
internal fun LicencesScreen(onBack: () -> Unit) {
internal fun LicencesScreen(onBack: () -> Unit, visible: Boolean) {
val uriHandler = LocalUriHandler.current
CollapsingScaffold(
title = stringResource(R.string.licences_title),
onBack = onBack,
predictiveBack = true,
visible = visible,
) {
Text(
stringResource(R.string.licences_intro),
@@ -3,12 +3,14 @@ package de.jeanlucmakiola.agendula.ui.navigation
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import de.jeanlucmakiola.agendula.domain.Task
import kotlin.time.Instant
@@ -27,6 +29,7 @@ import de.jeanlucmakiola.agendula.ui.lists.ListsScreen
import de.jeanlucmakiola.agendula.ui.settings.SettingsScreen
import de.jeanlucmakiola.agendula.ui.tasklist.TaskListScreen
import de.jeanlucmakiola.agendula.ui.tasklist.TaskListViewModel
import de.jeanlucmakiola.floret.identity.predictiveBackExit
/**
* The single in-app back stack: lists → task list → detail / edit.
@@ -37,6 +40,7 @@ import de.jeanlucmakiola.agendula.ui.tasklist.TaskListViewModel
* `SavedStateHandle`). Screens receive plain navigation lambdas so they stay
* unaware of the nav library.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AgendulaNavHost(
modifier: Modifier = Modifier,
@@ -70,19 +74,19 @@ fun AgendulaNavHost(
onNavRequestConsumed()
}
val slideSpec = MaterialTheme.motionScheme.defaultSpatialSpec<IntOffset>()
val overlayExit = predictiveBackExit()
NavHost(
navController = nav,
startDestination = Dest.LISTS,
modifier = modifier,
// Only the screen on top moves; the background is always held static.
// Forward is a plain fade-in over the held screen — calm, not a slide.
// Back is the predictive-back peek: navigation-compose composes the
// screen being revealed underneath and seeks popExit with the gesture, so
// the leaving screen scales down to reveal the (unmoved) previous screen.
enterTransition = { fadeIn() },
// Calendula's overlay motion. NavHost's own back handler seeks the exit
// over the previous screen, so destinations don't add a preview of their own.
enterTransition = { slideInHorizontally(slideSpec) { it } + fadeIn() },
exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None },
popExitTransition = { scaleOut(targetScale = 0.9f) + fadeOut() },
popExitTransition = { overlayExit },
) {
composable(Dest.LISTS) {
ReadableWidth {
@@ -34,6 +34,7 @@ internal fun AppearanceScreen(
state: SettingsUiState,
viewModel: SettingsViewModel,
onBack: () -> Unit,
visible: Boolean,
) {
val dynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
var showTheme by remember { mutableStateOf(false) }
@@ -42,7 +43,12 @@ internal fun AppearanceScreen(
val systemIs24Hour = DateFormat.is24HourFormat(LocalContext.current)
val locale = currentLocale()
CollapsingScaffold(title = stringResource(R.string.settings_section_appearance), onBack = onBack) {
CollapsingScaffold(
title = stringResource(R.string.settings_section_appearance),
onBack = onBack,
predictiveBack = true,
visible = visible,
) {
GroupedRow(
title = stringResource(R.string.settings_theme),
summary = stringResource(themeLabel(state.settings.themeMode)),
@@ -0,0 +1,37 @@
package de.jeanlucmakiola.agendula.ui.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import de.jeanlucmakiola.agendula.R
import de.jeanlucmakiola.floret.components.CollapsingScaffold
import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.Position
/** Backup & restore: the way into export and import, as its own entry (after Calendula). */
@Composable
internal fun BackupScreen(
onOpenExport: () -> Unit,
onOpenImport: () -> Unit,
onBack: () -> Unit,
visible: Boolean,
) {
CollapsingScaffold(
title = stringResource(R.string.settings_section_backup),
onBack = onBack,
predictiveBack = true,
visible = visible,
) {
GroupedRow(
title = stringResource(R.string.settings_export),
summary = stringResource(R.string.settings_export_hint),
position = Position.Top,
onClick = onOpenExport,
)
GroupedRow(
title = stringResource(R.string.import_title),
summary = stringResource(R.string.settings_import_hint),
position = Position.Bottom,
onClick = onOpenImport,
)
}
}
@@ -63,10 +63,11 @@ import java.time.LocalTime
/** Whether due tasks notify at all, how far ahead, and which lists differ. */
@Composable
internal fun RemindersScreen(
internal fun NotificationsScreen(
state: SettingsUiState,
viewModel: SettingsViewModel,
onBack: () -> Unit,
visible: Boolean,
) {
val context = LocalContext.current
var notificationsAllowed by remember { mutableStateOf(context.notificationsAllowed()) }
@@ -119,7 +120,12 @@ internal fun RemindersScreen(
)
val lists = state.lists.filter { it.id > 0L }
CollapsingScaffold(title = stringResource(R.string.settings_section_reminders), onBack = onBack) {
CollapsingScaffold(
title = stringResource(R.string.settings_section_reminders),
onBack = onBack,
predictiveBack = true,
visible = visible,
) {
if (state.settings.remindersEnabled && !notificationsAllowed) {
GroupedRow(
title = stringResource(R.string.settings_notifications_blocked),
@@ -1,11 +1,8 @@
package de.jeanlucmakiola.agendula.ui.settings
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
@@ -13,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Backup
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.Favorite
@@ -25,6 +23,7 @@ import androidx.compose.material.icons.filled.Translate
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material.icons.rounded.CloudSync
import androidx.compose.material.icons.rounded.Storage
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -37,6 +36,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -50,20 +50,22 @@ import de.jeanlucmakiola.floret.components.CollapsingScaffold
import de.jeanlucmakiola.floret.components.GroupedListInset
import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.GroupedSectionHeader
import de.jeanlucmakiola.floret.components.LanguagePickerRow
import de.jeanlucmakiola.floret.components.OptionPicker
import de.jeanlucmakiola.floret.components.Position
import de.jeanlucmakiola.floret.crash.CrashReportDialog
import de.jeanlucmakiola.floret.crash.CrashReporter
import de.jeanlucmakiola.floret.crash.openIssueTracker
import de.jeanlucmakiola.floret.crash.submitCrashReport
import de.jeanlucmakiola.floret.locale.AppLanguage
import de.jeanlucmakiola.floret.identity.predictiveBackExit
/** The settings sub-screens reached from the hub's category rows. */
internal enum class SettingsSection {
Appearance,
TaskForm,
Reminders,
Notifications,
Storage,
Backup,
Export,
Import,
Accounts,
@@ -72,10 +74,10 @@ internal enum class SettingsSection {
Licences,
;
/** Where back goes: Export is opened from Storage, the account screens from Accounts. */
/** The section that stays composed underneath: Backup under export and import, Accounts under its screens. */
val parent: SettingsSection?
get() = when (this) {
Export, Import -> Storage
Export, Import -> Backup
AddAccount, Account -> Accounts
else -> null
}
@@ -88,8 +90,9 @@ internal enum class SettingsSection {
* A full-screen destination; [onBack] pops it. [initialSection] names a
* sub-screen to open on (see [de.jeanlucmakiola.agendula.ui.navigation.Dest.Settings]).
*
* Every sub-screen is its own file. This one owns the hub, the section enum and
* the back rules between them, and nothing else.
* Every sub-screen is its own file and handles its own back through the
* predictive-back preview. This one owns the hub, the section enum and the
* routing between them, and nothing else.
*/
@Composable
fun SettingsScreen(
@@ -106,10 +109,8 @@ fun SettingsScreen(
else SettingsSection.entries.firstOrNull { it.name == initialSection },
)
}
// Inside a sub-screen, system back (button or gesture) returns to the hub
// rather than popping the whole Settings destination to the lists overview.
BackHandler(enabled = section != null) { section = section?.parent }
// Import's "open storage" comes back to Import rather than the hub.
var storageReturn by rememberSaveable { mutableStateOf<SettingsSection?>(null) }
Box(
modifier = modifier
@@ -118,34 +119,58 @@ fun SettingsScreen(
) {
SettingsHub(onBack = onBack, onOpenSection = { section = it })
SlideInSection(visible = section == SettingsSection.Appearance) {
AppearanceScreen(state = state, viewModel = viewModel, onBack = { section = null })
val appearance = section == SettingsSection.Appearance
SlideInSection(visible = appearance) {
AppearanceScreen(state = state, viewModel = viewModel, onBack = { section = null }, visible = appearance)
}
SlideInSection(visible = section == SettingsSection.TaskForm) {
TaskFormScreen(state = state, viewModel = viewModel, onBack = { section = null })
val taskForm = section == SettingsSection.TaskForm
SlideInSection(visible = taskForm) {
TaskFormScreen(state = state, viewModel = viewModel, onBack = { section = null }, visible = taskForm)
}
SlideInSection(visible = section == SettingsSection.Reminders) {
RemindersScreen(state = state, viewModel = viewModel, onBack = { section = null })
val notifications = section == SettingsSection.Notifications
SlideInSection(visible = notifications) {
NotificationsScreen(state = state, viewModel = viewModel, onBack = { section = null }, visible = notifications)
}
// Storage stays composed under Export, so the deeper screen slides over it.
val storageOpen = section == SettingsSection.Storage ||
section?.parent == SettingsSection.Storage
SlideInSection(visible = storageOpen) {
val storage = section == SettingsSection.Storage
SlideInSection(visible = storage) {
StorageScreen(
viewModel = viewModel,
onBack = {
section = storageReturn
storageReturn = null
},
visible = storage,
)
}
// Backup stays composed under export and import, so the deeper screen slides over it.
val backup = section == SettingsSection.Backup || section?.parent == SettingsSection.Backup
SlideInSection(visible = backup) {
BackupScreen(
onOpenExport = { section = SettingsSection.Export },
onOpenImport = { section = SettingsSection.Import },
onBack = { section = null },
visible = backup,
)
}
SlideInSection(visible = section == SettingsSection.Export) {
ExportScreen(onBack = { section = SettingsSection.Storage })
val exportOpen = section == SettingsSection.Export
SlideInSection(visible = exportOpen) {
ExportScreen(onBack = { section = SettingsSection.Backup }, visible = exportOpen)
}
SlideInSection(visible = section == SettingsSection.Import) {
ImportScreen(onBack = { section = SettingsSection.Storage })
val importOpen = section == SettingsSection.Import
SlideInSection(visible = importOpen) {
ImportScreen(
onBack = { section = SettingsSection.Backup },
onOpenStorage = {
storageReturn = SettingsSection.Import
section = SettingsSection.Storage
},
predictiveBack = true,
visible = importOpen,
)
}
SlideInSection(visible = section == SettingsSection.Licences) {
LicencesScreen(onBack = { section = null })
val licences = section == SettingsSection.Licences
SlideInSection(visible = licences) {
LicencesScreen(onBack = { section = null }, visible = licences)
}
AccountSections(
section = section,
@@ -155,12 +180,18 @@ fun SettingsScreen(
}
}
/**
* A sub-screen layered over the hub, with Calendula's overlay motion: it slides
* in on the motion scheme's spring and leaves from its predictive-back preview.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal fun SlideInSection(visible: Boolean, content: @Composable () -> Unit) {
val slideSpec = MaterialTheme.motionScheme.defaultSpatialSpec<IntOffset>()
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally { it } + fadeIn(),
exit = slideOutHorizontally { it } + fadeOut(),
enter = slideInHorizontally(slideSpec) { it } + fadeIn(),
exit = predictiveBackExit(),
) { content() }
}
@@ -208,7 +239,7 @@ private fun SettingsHub(
summary = stringResource(R.string.settings_reminders_subtitle),
position = Position.Bottom,
leading = { CategoryIcon(Icons.Default.Notifications, ChipAccent.Primary) },
onClick = { onOpenSection(SettingsSection.Reminders) },
onClick = { onOpenSection(SettingsSection.Notifications) },
)
Spacer(Modifier.height(8.dp))
@@ -225,14 +256,41 @@ private fun SettingsHub(
GroupedRow(
title = stringResource(R.string.settings_section_storage),
summary = stringResource(R.string.settings_storage_subtitle),
position = if (BuildConfig.SYNC_ENABLED) Position.Bottom else Position.Alone,
leading = { CategoryIcon(Icons.Rounded.Storage, ChipAccent.Secondary) },
position = if (BuildConfig.SYNC_ENABLED) Position.Middle else Position.Top,
leading = { CategoryIcon(Icons.Rounded.Storage, ChipAccent.Primary) },
onClick = { onOpenSection(SettingsSection.Storage) },
)
GroupedRow(
title = stringResource(R.string.settings_section_backup),
summary = stringResource(R.string.settings_backup_subtitle),
position = Position.Bottom,
leading = { CategoryIcon(Icons.Default.Backup, ChipAccent.Secondary) },
onClick = { onOpenSection(SettingsSection.Backup) },
)
Spacer(Modifier.height(8.dp))
GroupedSectionHeader(stringResource(R.string.settings_group_app))
LanguageRow(position = Position.Top)
LanguagePickerRow(
position = Position.Top,
title = stringResource(R.string.settings_language),
autoLabel = stringResource(R.string.settings_language_auto),
localesConfig = R.xml.locales_config,
predictiveBack = true,
// Invite contributions right where users pick their language.
header = {
val context = LocalContext.current
val translateUrl = stringResource(R.string.about_translate_url)
GroupedRow(
title = stringResource(R.string.settings_translate),
summary = stringResource(R.string.settings_translate_hint),
position = Position.Alone,
leading = { CategoryIcon(Icons.Default.Translate, ChipAccent.Neutral) },
onClick = { openUrl(context, translateUrl) },
)
Spacer(Modifier.height(16.dp))
},
leading = { CategoryIcon(Icons.Default.Language, ChipAccent.Secondary) },
)
ReportProblemRow(position = Position.Bottom)
// Source, licence and privacy sit at the bottom as reference material
@@ -316,64 +374,6 @@ private fun AboutLinkRow(label: String, url: String, icon: ImageVector, position
)
}
/**
* The app-language row. Deliberately not floret-kit's `LanguagePickerRow`: the
* picker it opens carries a "Help translate" header, and inviting contributions
* right where a user goes looking for their language is app-specific framing,
* not a family primitive. Everything else matches that recipe.
*/
@Composable
private fun LanguageRow(position: Position) {
val context = LocalContext.current
val supported = remember { AppLanguage.supportedTags(context, R.xml.locales_config) }
// Setting a locale recreates the activity; mirror the choice locally so the
// row updates instantly even before the recreation lands.
var current by remember { mutableStateOf(AppLanguage.currentTag(supported)) }
var showDialog by remember { mutableStateOf(false) }
// null = follow the system; the rest are BCP-47 tags from locales_config.xml.
val options = remember(supported) { listOf<String?>(null) + supported }
GroupedRow(
title = stringResource(R.string.settings_language),
summary = languageLabel(current),
position = position,
leading = { CategoryIcon(Icons.Default.Language, ChipAccent.Secondary) },
onClick = { showDialog = true },
)
if (showDialog) {
OptionPicker(
title = stringResource(R.string.settings_language),
predictiveBack = true,
options = options,
selected = current,
label = { languageLabel(it) },
onSelect = {
current = it
AppLanguage.apply(it)
},
onDismiss = { showDialog = false },
// Invite contributions right where users pick their language.
header = {
val translateUrl = stringResource(R.string.about_translate_url)
GroupedRow(
title = stringResource(R.string.settings_translate),
summary = stringResource(R.string.settings_translate_hint),
position = Position.Alone,
leading = { CategoryIcon(Icons.Default.Translate, ChipAccent.Neutral) },
onClick = { openUrl(context, translateUrl) },
)
Spacer(Modifier.height(16.dp))
},
)
}
}
@Composable
private fun languageLabel(tag: String?): String =
if (tag == null) stringResource(R.string.settings_language_auto) else AppLanguage.displayName(tag)
/**
* Opens the project's issue tracker to report a problem. If a crash report was
* captured (and not yet sent), it surfaces that report first via the same
@@ -55,9 +55,8 @@ import de.jeanlucmakiola.floret.components.SelectedCheck
@Composable
internal fun StorageScreen(
viewModel: SettingsViewModel,
onOpenExport: () -> Unit,
onOpenImport: () -> Unit,
onBack: () -> Unit,
visible: Boolean,
) {
val context = LocalContext.current
val storage by viewModel.storage.collectAsStateWithLifecycle()
@@ -85,11 +84,17 @@ internal fun StorageScreen(
// permission dialog too, and would wipe the refusal before it is read.
OnResume { viewModel.refreshStorage() }
CollapsingScaffold(title = stringResource(R.string.settings_section_storage), onBack = onBack) {
CollapsingScaffold(
title = stringResource(R.string.settings_section_storage),
onBack = onBack,
predictiveBack = true,
visible = visible,
) {
val canCopy = storage?.canCopyFromExternal == true
GroupedRow(
title = stringResource(R.string.settings_task_store),
summary = storage?.let { storeLabel(it) },
position = Position.Top,
position = if (canCopy) Position.Top else Position.Alone,
// Nothing to pick until the stored mode has landed; opening the picker
// on the seeded state would offer the wrong store as the current one.
onClick = storage?.let {
@@ -99,31 +104,19 @@ internal fun StorageScreen(
}
},
)
if (storage?.canCopyFromExternal == true) {
if (canCopy) {
GroupedRow(
title = stringResource(
R.string.settings_copy_from_external,
storage?.let { externalTitle(it.external, it.externalLabel) }.orEmpty(),
),
summary = stringResource(R.string.settings_copy_from_external_hint),
position = Position.Middle,
position = Position.Bottom,
leading = { Icon(Icons.Rounded.MoveDown, contentDescription = null) },
// Inert while a copy is in flight; the receipt below says so.
onClick = if (transfer.running) null else viewModel::startCopyFromExternal,
)
}
GroupedRow(
title = stringResource(R.string.settings_export),
summary = stringResource(R.string.settings_export_hint),
position = Position.Middle,
onClick = onOpenExport,
)
GroupedRow(
title = stringResource(R.string.import_title),
summary = stringResource(R.string.settings_import_hint),
position = Position.Bottom,
onClick = onOpenImport,
)
TransferStatus(transfer)
@@ -42,10 +42,16 @@ internal fun TaskFormScreen(
state: SettingsUiState,
viewModel: SettingsViewModel,
onBack: () -> Unit,
visible: Boolean,
) {
var showDefaultList by remember { mutableStateOf(false) }
CollapsingScaffold(title = stringResource(R.string.settings_section_task_form), onBack = onBack) {
CollapsingScaffold(
title = stringResource(R.string.settings_section_task_form),
onBack = onBack,
predictiveBack = true,
visible = visible,
) {
SettingsHint(stringResource(R.string.settings_form_fields_hint))
Spacer(Modifier.height(8.dp))
val fields = TaskFormField.entries
+15 -13
View File
@@ -122,8 +122,8 @@
<string name="reminder_unit_days">Days</string>
<string name="reminder_unit_weeks">Weeks</string>
<string name="settings_list_reminders_title">Per-list reminders</string>
<string name="settings_list_reminders_hint">Override the default reminder for individual lists</string>
<string name="settings_list_notifications">Notifications</string>
<string name="settings_list_reminders_hint">Override the default per list — separately for timed and all-day tasks. A list can keep the default, drop it, or set its own.</string>
<string name="settings_list_notifications">Sound &amp; vibration</string>
<string name="settings_list_notifications_hint">Sound, vibration and importance for this list’s reminders</string>
<string name="settings_list_reminder_inherits">Default (%1$s)</string>
@@ -359,7 +359,7 @@
<!-- First run: backup -->
<string name="onboarding_backup_title">Keep a copy</string>
<string name="onboarding_backup_body_later">Without an account, your lists live only on this device. Save them as iCalendar files any other task or calendar app can read, any time from Settings → Storage → Export.</string>
<string name="onboarding_backup_body_later">Without an account, your lists live only on this device. Save them as iCalendar files any other task or calendar app can read, any time from Settings → Backup &amp; restore → Export.</string>
<string name="onboarding_backup_continue">Continue</string>
<!-- First run: quick add bar -->
@@ -394,10 +394,10 @@
<!-- Group of reference links at the foot of the settings hub. -->
<string name="settings_group_about">About</string>
<string name="settings_section_appearance">Appearance</string>
<string name="settings_appearance_subtitle">Theme, colour and time format</string>
<string name="settings_section_task_form">Task form</string>
<string name="settings_task_form_subtitle">Default fields, list and subtasks</string>
<string name="settings_reminders_subtitle">Notifications for due tasks</string>
<string name="settings_appearance_subtitle">Theme, colours, time format</string>
<string name="settings_section_task_form">New task form</string>
<string name="settings_task_form_subtitle">Default fields and behaviour</string>
<string name="settings_reminders_subtitle">Reminders and delivery</string>
<string name="settings_form_fields_hint">Fields shown by default on a new task. The rest sit behind \"More fields\".</string>
<string name="settings_dynamic_color_unavailable">Available on Android 12 and later</string>
<string name="settings_autofocus_title">Focus title on new task</string>
@@ -442,13 +442,13 @@
<string name="settings_theme_dark">Dark</string>
<string name="settings_dynamic_color">Dynamic colour</string>
<string name="settings_dynamic_color_hint">Use colours from your wallpaper</string>
<string name="settings_section_reminders">Reminders</string>
<string name="settings_reminders">Due reminders</string>
<string name="settings_section_reminders">Notifications</string>
<string name="settings_reminders">Task reminders</string>
<string name="settings_reminders_hint">Notify me when a task is due</string>
<string name="settings_notifications_blocked">Notifications are off</string>
<string name="settings_notifications_blocked_hint">Reminders can\u2019t reach you. Tap to allow notifications for Agendula.</string>
<string name="settings_default_reminder">When to remind</string>
<string name="settings_allday_reminder_time">Time for all-day reminders</string>
<string name="settings_default_reminder">Default reminder</string>
<string name="settings_allday_reminder_time">All-day reminder time</string>
<string name="settings_allday_reminder_time_summary">All-day tasks count back from %1$s</string>
<string name="settings_default_reminder_allday">All-day tasks</string>
<string name="settings_allday_reminder_fires_at">Notifies at %1$s</string>
@@ -472,9 +472,11 @@
<!-- Storage and export -->
<string name="settings_section_storage">Storage</string>
<string name="settings_storage_subtitle">Where tasks are kept, and export</string>
<string name="settings_storage_subtitle">Where your tasks are kept</string>
<string name="settings_section_backup">Backup &amp; restore</string>
<string name="settings_backup_subtitle">Export and import</string>
<string name="settings_task_store">Task store</string>
<string name="settings_task_store_hint">Each store keeps its own tasks. Switching does not move them across — copy them over first, or export them.</string>
<string name="settings_task_store_hint">Each store keeps its own tasks. Switching does not move them across — copy them over first, or export them from Backup &amp; restore.</string>
<string name="settings_store_own">On this device</string>
<string name="settings_store_own_hint">Agendula\'s own storage. Nothing else to install.</string>
<string name="settings_store_external">Another task app</string>