From 265de47274f5c6184e618d5a185b8efd806d5e26 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 9 Sep 2026 15:18:05 +0200 Subject: [PATCH] settings: four named groups, and every sub-screen in its own file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hub was a flat run of nine rows — Appearance, Task form, Reminders, Storage, Accounts, Language, Licences, Privacy, Report a problem — with nothing putting sync next to storage and nothing separating how the app behaves from what the app is. It is four named groups now, each a connected run with one accent chip colour: Appearance (theme and colour, language), Tasks (task form, reminders), Sync and data (accounts, storage), About (licences, privacy, report a problem). The Appearance row takes the wording that used to be its summary — under a header of the same name the old title said nothing, and that string is already translated everywhere. Two new headers, so two new strings; "Tasks" reuses the key the German and Brazilian catalogues already carry. And the 814-line file held the hub plus AppearanceScreen, TaskFormScreen and RemindersScreen while Storage, Export and the account screens lived in their own files, so where a sub-screen lived was arbitrary. Each of the three is its own file now, the shared chips and about bits are in SettingsCommon, and SettingsScreen keeps the hub, the section enum and the back rules between them. --- .../agendula/ui/settings/AppearanceScreen.kt | 74 +++ .../agendula/ui/settings/RemindersScreen.kt | 216 +++++++ .../agendula/ui/settings/SettingsCommon.kt | 93 +++ .../agendula/ui/settings/SettingsScreen.kt | 534 ++---------------- .../agendula/ui/settings/TaskFormScreen.kt | 145 +++++ app/src/main/res/values/strings.xml | 3 + floret-kit | 2 +- 7 files changed, 580 insertions(+), 487 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/RemindersScreen.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsCommon.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/TaskFormScreen.kt diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt new file mode 100644 index 0000000..0b72f30 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt @@ -0,0 +1,74 @@ +package de.jeanlucmakiola.agendula.ui.settings + +import android.os.Build +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import de.jeanlucmakiola.agendula.R +import de.jeanlucmakiola.agendula.data.prefs.ThemeMode +import de.jeanlucmakiola.floret.components.CollapsingScaffold +import de.jeanlucmakiola.floret.components.GroupedRow +import de.jeanlucmakiola.floret.components.OptionPicker +import de.jeanlucmakiola.floret.components.Position + +/** How the app looks: the theme, and whether it takes its colours from the wallpaper. */ +@Composable +internal fun AppearanceScreen( + state: SettingsUiState, + viewModel: SettingsViewModel, + onBack: () -> Unit, +) { + val dynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + var showTheme by remember { mutableStateOf(false) } + + CollapsingScaffold(title = stringResource(R.string.settings_section_appearance), onBack = onBack) { + GroupedRow( + title = stringResource(R.string.settings_theme), + summary = stringResource(themeLabel(state.settings.themeMode)), + position = Position.Top, + onClick = { showTheme = true }, + ) + GroupedRow( + title = stringResource(R.string.settings_dynamic_color), + summary = if (dynamicColorAvailable) { + stringResource(R.string.settings_dynamic_color_hint) + } else { + stringResource(R.string.settings_dynamic_color_unavailable) + }, + position = Position.Bottom, + trailing = { + Switch( + checked = state.settings.dynamicColor && dynamicColorAvailable, + onCheckedChange = viewModel::setDynamicColor, + enabled = dynamicColorAvailable, + ) + }, + onClick = if (dynamicColorAvailable) { + { viewModel.setDynamicColor(!state.settings.dynamicColor) } + } else { + null + }, + ) + } + + if (showTheme) { + OptionPicker( + title = stringResource(R.string.settings_theme), + options = ThemeMode.entries, + selected = state.settings.themeMode, + label = { stringResource(themeLabel(it)) }, + onSelect = viewModel::setThemeMode, + onDismiss = { showTheme = false }, + ) + } +} + +private fun themeLabel(mode: ThemeMode): Int = when (mode) { + ThemeMode.SYSTEM -> R.string.settings_theme_system + ThemeMode.LIGHT -> R.string.settings_theme_light + ThemeMode.DARK -> R.string.settings_theme_dark +} diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/RemindersScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/RemindersScreen.kt new file mode 100644 index 0000000..d2bf754 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/RemindersScreen.kt @@ -0,0 +1,216 @@ +package de.jeanlucmakiola.agendula.ui.settings + +import android.Manifest +import android.app.AlarmManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.rounded.Circle +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import de.jeanlucmakiola.agendula.R +import de.jeanlucmakiola.agendula.ui.common.OnResume +import de.jeanlucmakiola.agendula.ui.common.ReminderLeadPicker +import de.jeanlucmakiola.agendula.ui.common.reminderLeadTimeLabel +import de.jeanlucmakiola.floret.components.CollapsingScaffold +import de.jeanlucmakiola.floret.components.GroupedRow +import de.jeanlucmakiola.floret.components.Position +import de.jeanlucmakiola.floret.components.pastelize +import de.jeanlucmakiola.floret.identity.collapseExit +import de.jeanlucmakiola.floret.identity.expandEnter +import de.jeanlucmakiola.floret.reminders.ReminderOverride +import de.jeanlucmakiola.floret.reminders.reminderOverrideFor + +/** Whether due tasks notify at all, how far ahead, and which lists differ. */ +@Composable +internal fun RemindersScreen( + state: SettingsUiState, + viewModel: SettingsViewModel, + onBack: () -> Unit, +) { + val context = LocalContext.current + val notifLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { /* pref already set; a denial just leaves the OS gate shut */ } + val toggleReminders: (Boolean) -> Unit = { enabled -> + viewModel.setRemindersEnabled(enabled) + if (enabled && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED + ) { + notifLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + val canExact = rememberExactAlarmAllowed(context) + val exactAlarmRow = showExactAlarmRow() + val dark = isSystemInDarkTheme() + var showOffset by remember { mutableStateOf(false) } + var perListExpanded by remember { mutableStateOf(false) } + var listPickerId by remember { mutableStateOf(null) } + val lists = state.lists.filter { it.id > 0L } + + CollapsingScaffold(title = stringResource(R.string.settings_section_reminders), onBack = onBack) { + GroupedRow( + title = stringResource(R.string.settings_reminders), + summary = stringResource(R.string.settings_reminders_hint), + position = Position.Top, + trailing = { Switch(checked = state.settings.remindersEnabled, onCheckedChange = toggleReminders) }, + onClick = { toggleReminders(!state.settings.remindersEnabled) }, + ) + GroupedRow( + title = stringResource(R.string.settings_default_reminder), + summary = reminderLeadTimeLabel(state.settings.reminderLeadMinutes), + position = if (exactAlarmRow) Position.Middle else Position.Bottom, + onClick = { showOffset = true }, + ) + if (exactAlarmRow) { + GroupedRow( + title = stringResource(R.string.settings_exact_alarms), + summary = stringResource( + if (canExact) R.string.settings_exact_alarms_allowed + else R.string.settings_exact_alarms_blocked, + ), + position = Position.Bottom, + onClick = if (canExact) null else ({ context.openExactAlarmSettings() }), + ) + } + + // Per-list overrides: the whole section folds behind one header. Each + // list may keep, drop, or replace the global default for its tasks. + if (lists.isNotEmpty()) { + Spacer(Modifier.height(24.dp)) + // The header and the list rows form one connected run: the header + // opens its bottom (Top) when expanded, and the last list rounds it off. + GroupedRow( + title = stringResource(R.string.settings_list_reminders_title), + summary = stringResource(R.string.settings_list_reminders_hint), + position = if (perListExpanded) Position.Top else Position.Alone, + trailing = { + Icon( + imageVector = if (perListExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + onClick = { perListExpanded = !perListExpanded }, + ) + AnimatedVisibility( + visible = perListExpanded, + enter = expandEnter(), + exit = collapseExit(), + ) { + Column { + lists.forEachIndexed { index, list -> + GroupedRow( + title = list.name, + summary = listOverrideSummary( + listOverrideChoice(state, list.id), + state.settings.reminderLeadMinutes, + ), + // Every list is mid-run under the header; the last rounds off. + position = if (index == lists.lastIndex) Position.Bottom else Position.Middle, + leading = { + Icon(Icons.Rounded.Circle, contentDescription = null, tint = pastelize(list.color, dark)) + }, + onClick = { listPickerId = list.id }, + ) + } + } + } + } + } + + if (showOffset) { + ReminderLeadPicker( + title = stringResource(R.string.settings_default_reminder), + selected = ReminderOverride.Minutes(listOf(state.settings.reminderLeadMinutes)), + allowInherit = false, + allowNone = false, + onSelect = { if (it is ReminderOverride.Minutes) viewModel.setReminderLeadMinutes(it.minutes.first()) }, + onDismiss = { showOffset = false }, + ) + } + listPickerId?.let { id -> + ReminderLeadPicker( + title = state.lists.firstOrNull { it.id == id }?.name + ?: stringResource(R.string.settings_default_reminder), + selected = listOverrideChoice(state, id), + allowInherit = true, + allowNone = true, + onSelect = { viewModel.setListReminderOverride(id, it) }, + onDismiss = { listPickerId = null }, + ) + } +} + +/** The stored override for [listId], as a picker choice (absent → inherit). */ +private fun listOverrideChoice(state: SettingsUiState, listId: Long): ReminderOverride = + state.settings.perListReminderOverride.reminderOverrideFor(listId) + +/** Row summary for a list: its override, or the inherited global default. */ +@Composable +private fun listOverrideSummary(choice: ReminderOverride, globalDefault: Int): String = when (choice) { + ReminderOverride.Inherit -> + stringResource(R.string.settings_list_reminder_inherits, reminderLeadTimeLabel(globalDefault)) + ReminderOverride.None -> stringResource(R.string.reminder_none) + is ReminderOverride.Minutes -> reminderLeadTimeLabel(choice.minutes.first()) +} + +/** + * Exact-alarm control is only meaningful on API 31-32 (SCHEDULE_EXACT_ALARM, + * revocable). On 33+ the app holds USE_EXACT_ALARM (always granted), so the row + * would be a permanent no-op — hide it there. + */ +private fun showExactAlarmRow(): Boolean = + Build.VERSION.SDK_INT in Build.VERSION_CODES.S..Build.VERSION_CODES.S_V2 + +@Composable +private fun rememberExactAlarmAllowed(context: Context): Boolean { + var allowed by remember { + mutableStateOf( + Build.VERSION.SDK_INT < Build.VERSION_CODES.S || + context.getSystemService(AlarmManager::class.java).canScheduleExactAlarms(), + ) + } + OnResume { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + allowed = context.getSystemService(AlarmManager::class.java).canScheduleExactAlarms() + } + } + return allowed +} + +private fun Context.openExactAlarmSettings() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + runCatching { + startActivity( + Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).setData("package:$packageName".toUri()), + ) + } + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsCommon.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsCommon.kt new file mode 100644 index 0000000..67bfdf3 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsCommon.kt @@ -0,0 +1,93 @@ +package de.jeanlucmakiola.agendula.ui.settings + +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import de.jeanlucmakiola.agendula.R + +/** + * Token-based accent for a leading icon chip (container / on-container pair), + * so each accent stays correctly paired across theme, dark mode and dynamic + * colour. One accent per hub section, so a section reads as a colour band + * rather than nine chips picked one at a time. + */ +internal enum class ChipAccent { Neutral, Primary, Secondary, Tertiary } + +/** Leading circular icon chip; colours come from the M3 scheme via a token pair. */ +@Composable +internal fun CategoryIcon(icon: ImageVector, accent: ChipAccent) { + val scheme = MaterialTheme.colorScheme + val (background, iconColor) = when (accent) { + ChipAccent.Neutral -> scheme.surfaceContainerHighest to scheme.onSurfaceVariant + ChipAccent.Primary -> scheme.primaryContainer to scheme.onPrimaryContainer + ChipAccent.Secondary -> scheme.secondaryContainer to scheme.onSecondaryContainer + ChipAccent.Tertiary -> scheme.tertiaryContainer to scheme.onTertiaryContainer + } + Box( + modifier = Modifier.size(40.dp).clip(CircleShape).background(background), + contentAlignment = Alignment.Center, + ) { + Icon(icon, contentDescription = null, tint = iconColor, modifier = Modifier.size(22.dp)) + } +} + +@Composable +internal fun AppLogo() { + Box( + modifier = Modifier + .size(72.dp) + .clip(RoundedCornerShape(20.dp)) + .background(colorResource(R.color.ic_launcher_background)), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = stringResource(R.string.settings_about_logo_desc), + modifier = Modifier.requiredSize(108.dp), + ) + } +} + +@Composable +internal fun AppVersionText() { + val context = LocalContext.current + val versionName = remember { + runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName } + .getOrNull() ?: "—" + } + Text( + text = stringResource(R.string.settings_about_version, versionName), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + ) +} + +internal fun openUrl(context: Context, url: String) { + runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) } +} diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsScreen.kt index 91dfb01..d879a0b 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsScreen.kt @@ -1,109 +1,59 @@ package de.jeanlucmakiola.agendula.ui.settings -import android.Manifest -import android.app.AlarmManager -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.provider.Settings import androidx.activity.compose.BackHandler -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts 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.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.Image import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Notes import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.Code -import androidx.compose.material.icons.filled.ExpandLess -import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Gavel -import androidx.compose.material.icons.filled.PrivacyTip import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Palette +import androidx.compose.material.icons.filled.PrivacyTip import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.Tune -import androidx.compose.material.icons.rounded.AccountTree -import androidx.compose.material.icons.rounded.Circle -import androidx.compose.material.icons.rounded.Flag -import androidx.compose.material.icons.rounded.Percent import androidx.compose.material.icons.rounded.CloudSync import androidx.compose.material.icons.rounded.Storage -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.agendula.R -import de.jeanlucmakiola.agendula.ui.licences.LicencesScreen -import de.jeanlucmakiola.agendula.data.prefs.ThemeMode -import de.jeanlucmakiola.agendula.domain.TaskFormField -import de.jeanlucmakiola.agendula.ui.export.ExportScreen -import de.jeanlucmakiola.floret.components.AboutCard -import de.jeanlucmakiola.floret.components.AboutLink -import de.jeanlucmakiola.floret.components.CollapsingScaffold -import de.jeanlucmakiola.floret.components.GroupedRow -import de.jeanlucmakiola.floret.components.OptionPicker -import de.jeanlucmakiola.floret.components.Position -import de.jeanlucmakiola.agendula.ui.common.ReminderLeadPicker -import de.jeanlucmakiola.floret.components.pastelize -import de.jeanlucmakiola.floret.components.positionOf -import de.jeanlucmakiola.floret.identity.collapseExit -import de.jeanlucmakiola.floret.locale.AppLanguage -import de.jeanlucmakiola.floret.identity.expandEnter -import de.jeanlucmakiola.floret.reminders.ReminderOverride -import de.jeanlucmakiola.floret.reminders.reminderOverrideFor -import de.jeanlucmakiola.agendula.ui.common.OnResume import de.jeanlucmakiola.agendula.ui.accounts.AccountDetailScreen import de.jeanlucmakiola.agendula.ui.accounts.AccountsScreen import de.jeanlucmakiola.agendula.ui.accounts.AccountsViewModel import de.jeanlucmakiola.agendula.ui.accounts.AddAccountScreen -import de.jeanlucmakiola.agendula.ui.common.reminderLeadTimeLabel +import de.jeanlucmakiola.agendula.ui.export.ExportScreen +import de.jeanlucmakiola.agendula.ui.licences.LicencesScreen +import de.jeanlucmakiola.floret.components.AboutCard +import de.jeanlucmakiola.floret.components.AboutLink +import de.jeanlucmakiola.floret.components.CollapsingScaffold +import de.jeanlucmakiola.floret.components.GroupedRow +import de.jeanlucmakiola.floret.components.GroupedSectionHeader +import de.jeanlucmakiola.floret.components.OptionPicker +import de.jeanlucmakiola.floret.components.Position +import de.jeanlucmakiola.floret.locale.AppLanguage /** The settings sub-screens reached from the hub's category rows. */ private enum class SettingsSection { @@ -127,18 +77,14 @@ private enum class SettingsSection { } } -/** - * Token-based accent for a leading icon chip (container / on-container pair), - * so each accent stays correctly paired across theme, dark mode and dynamic - * colour. - */ -private enum class ChipAccent { Neutral, Primary, Tertiary } - /** * Settings, structured (after Calendula) as a category hub with sliding * sub-screens. The hub and the sub-screens share [CollapsingScaffold] and the * grouped-row card system; option lists use the full-screen [OptionPicker]. * A full-screen destination; [onBack] pops it. + * + * Every sub-screen is its own file. This one owns the hub, the section enum and + * the back rules between them, and nothing else. */ @Composable fun SettingsScreen( @@ -241,6 +187,12 @@ private fun SlideInSection(visible: Boolean, content: @Composable () -> Unit) { // Hub // --------------------------------------------------------------------------- +/** + * Four named groups, each a connected run of rows with one accent: what the app + * looks like, what a task looks like, where the tasks live and who syncs them, + * and what the app itself is. The About card stays on top — it is the page's + * identity, not one of the categories. + */ @Composable private fun SettingsHub( onBack: () -> Unit, @@ -271,48 +223,56 @@ private fun SettingsHub( ), ) } - Spacer(Modifier.height(16.dp)) + GroupedSectionHeader(stringResource(R.string.settings_section_appearance)) GroupedRow( - title = stringResource(R.string.settings_section_appearance), - summary = stringResource(R.string.settings_appearance_subtitle), + // The old row's *summary*, promoted: under an "Appearance" header a + // row also called Appearance says nothing, and this wording is + // already translated everywhere. + title = stringResource(R.string.settings_appearance_subtitle), position = Position.Top, - leading = { CategoryIcon(Icons.Default.Palette, ChipAccent.Neutral) }, + leading = { CategoryIcon(Icons.Default.Palette, ChipAccent.Primary) }, onClick = { onOpenSection(SettingsSection.Appearance) }, ) + LanguageRow(position = Position.Bottom, accent = ChipAccent.Primary) + + GroupedSectionHeader(stringResource(R.string.settings_section_tasks)) GroupedRow( title = stringResource(R.string.settings_section_task_form), summary = stringResource(R.string.settings_task_form_subtitle), - position = Position.Middle, + position = Position.Top, leading = { CategoryIcon(Icons.Default.Tune, ChipAccent.Tertiary) }, onClick = { onOpenSection(SettingsSection.TaskForm) }, ) GroupedRow( title = stringResource(R.string.settings_section_reminders), summary = stringResource(R.string.settings_reminders_subtitle), - position = Position.Middle, - leading = { CategoryIcon(Icons.Default.Notifications, ChipAccent.Primary) }, + position = Position.Bottom, + leading = { CategoryIcon(Icons.Default.Notifications, ChipAccent.Tertiary) }, onClick = { onOpenSection(SettingsSection.Reminders) }, ) + + GroupedSectionHeader(stringResource(R.string.settings_section_sync)) + GroupedRow( + title = stringResource(R.string.settings_section_accounts), + summary = stringResource(R.string.settings_accounts_subtitle), + position = Position.Top, + leading = { CategoryIcon(Icons.Rounded.CloudSync, ChipAccent.Secondary) }, + onClick = { onOpenSection(SettingsSection.Accounts) }, + ) GroupedRow( title = stringResource(R.string.settings_section_storage), summary = stringResource(R.string.settings_storage_subtitle), - position = Position.Middle, - leading = { CategoryIcon(Icons.Rounded.Storage, ChipAccent.Neutral) }, + position = Position.Bottom, + leading = { CategoryIcon(Icons.Rounded.Storage, ChipAccent.Secondary) }, onClick = { onOpenSection(SettingsSection.Storage) }, ) - GroupedRow( - title = stringResource(R.string.settings_section_accounts), - summary = stringResource(R.string.settings_accounts_subtitle), - position = Position.Middle, - leading = { CategoryIcon(Icons.Rounded.CloudSync, ChipAccent.Tertiary) }, - onClick = { onOpenSection(SettingsSection.Accounts) }, - ) - LanguageRow(position = Position.Middle) + + GroupedSectionHeader(stringResource(R.string.settings_section_about)) GroupedRow( title = stringResource(R.string.settings_licences), summary = stringResource(R.string.settings_licences_subtitle), - position = Position.Middle, + position = Position.Top, leading = { CategoryIcon(Icons.Default.Gavel, ChipAccent.Neutral) }, onClick = { onOpenSection(SettingsSection.Licences) }, ) @@ -351,7 +311,7 @@ private fun PrivacyPolicyRow(position: Position) { * not a family primitive. Everything else matches that recipe. */ @Composable -private fun LanguageRow(position: Position) { +private fun LanguageRow(position: Position, accent: ChipAccent) { val context = LocalContext.current // Setting a locale recreates the activity; mirror the choice locally so the // row updates instantly even before the recreation lands. @@ -365,7 +325,7 @@ private fun LanguageRow(position: Position) { title = stringResource(R.string.settings_language), summary = languageLabel(current), position = position, - leading = { CategoryIcon(Icons.Default.Language, ChipAccent.Neutral) }, + leading = { CategoryIcon(Icons.Default.Language, accent) }, onClick = { showDialog = true }, ) @@ -414,401 +374,3 @@ private fun ReportProblemRow(position: Position) { onClick = { openUrl(context, issueUrl) }, ) } - -// --------------------------------------------------------------------------- -// Sub-screens -// --------------------------------------------------------------------------- - -@Composable -private fun AppearanceScreen( - state: SettingsUiState, - viewModel: SettingsViewModel, - onBack: () -> Unit, -) { - val dynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - var showTheme by remember { mutableStateOf(false) } - - CollapsingScaffold(title = stringResource(R.string.settings_section_appearance), onBack = onBack) { - GroupedRow( - title = stringResource(R.string.settings_theme), - summary = stringResource(themeLabel(state.settings.themeMode)), - position = Position.Top, - onClick = { showTheme = true }, - ) - GroupedRow( - title = stringResource(R.string.settings_dynamic_color), - summary = if (dynamicColorAvailable) { - stringResource(R.string.settings_dynamic_color_hint) - } else { - stringResource(R.string.settings_dynamic_color_unavailable) - }, - position = Position.Bottom, - trailing = { - Switch( - checked = state.settings.dynamicColor && dynamicColorAvailable, - onCheckedChange = viewModel::setDynamicColor, - enabled = dynamicColorAvailable, - ) - }, - onClick = if (dynamicColorAvailable) { - { viewModel.setDynamicColor(!state.settings.dynamicColor) } - } else { - null - }, - ) - } - - if (showTheme) { - OptionPicker( - title = stringResource(R.string.settings_theme), - options = ThemeMode.entries, - selected = state.settings.themeMode, - label = { stringResource(themeLabel(it)) }, - onSelect = viewModel::setThemeMode, - onDismiss = { showTheme = false }, - ) - } -} - -@Composable -private fun TaskFormScreen( - state: SettingsUiState, - viewModel: SettingsViewModel, - onBack: () -> Unit, -) { - var showDefaultList by remember { mutableStateOf(false) } - - CollapsingScaffold(title = stringResource(R.string.settings_section_task_form), onBack = onBack) { - Text( - text = stringResource(R.string.settings_form_fields_hint), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), - ) - Spacer(Modifier.height(8.dp)) - val fields = TaskFormField.entries - fields.forEachIndexed { index, field -> - val checked = field in state.settings.defaultEditFields - GroupedRow( - title = stringResource(formFieldLabel(field)), - position = positionOf(index, fields.size), - leading = { - Icon( - imageVector = formFieldIcon(field), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - trailing = { - Switch(checked = checked, onCheckedChange = { viewModel.setFormFieldDefault(field, it) }) - }, - onClick = { viewModel.setFormFieldDefault(field, !checked) }, - ) - } - - Spacer(Modifier.height(24.dp)) - GroupedRow( - title = stringResource(R.string.settings_default_list), - summary = defaultListLabel(state), - position = Position.Top, - onClick = { showDefaultList = true }, - ) - GroupedRow( - title = stringResource(R.string.settings_add_subtask_row), - summary = stringResource(R.string.settings_add_subtask_row_hint), - position = Position.Middle, - trailing = { - Switch( - checked = state.settings.showAddSubtaskRow, - onCheckedChange = viewModel::setShowAddSubtaskRow, - ) - }, - onClick = { viewModel.setShowAddSubtaskRow(!state.settings.showAddSubtaskRow) }, - ) - GroupedRow( - title = stringResource(R.string.settings_bottom_add_bar), - summary = stringResource(R.string.settings_bottom_add_bar_hint), - position = Position.Bottom, - trailing = { - Switch( - checked = state.settings.bottomAddBar, - onCheckedChange = viewModel::setBottomAddBar, - ) - }, - onClick = { viewModel.setBottomAddBar(!state.settings.bottomAddBar) }, - ) - } - - if (showDefaultList) { - val dark = isSystemInDarkTheme() - // null = first available list; the rest are real list ids. - val options = remember(state.lists) { listOf(null) + state.lists.map { it.id } } - OptionPicker( - title = stringResource(R.string.settings_default_list), - options = options, - selected = state.settings.defaultListId, - label = { id -> state.lists.firstOrNull { it.id == id }?.name ?: stringResource(R.string.settings_default_list_first) }, - leading = { id -> - state.lists.firstOrNull { it.id == id }?.let { list -> - Icon(Icons.Rounded.Circle, contentDescription = null, tint = pastelize(list.color, dark)) - } - }, - onSelect = { viewModel.setDefaultList(it) }, - onDismiss = { showDefaultList = false }, - ) - } -} - -@Composable -private fun RemindersScreen( - state: SettingsUiState, - viewModel: SettingsViewModel, - onBack: () -> Unit, -) { - val context = LocalContext.current - val notifLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - ) { /* pref already set; a denial just leaves the OS gate shut */ } - val toggleReminders: (Boolean) -> Unit = { enabled -> - viewModel.setRemindersEnabled(enabled) - if (enabled && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != - PackageManager.PERMISSION_GRANTED - ) { - notifLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - } - } - val canExact = rememberExactAlarmAllowed(context) - val exactAlarmRow = showExactAlarmRow() - val dark = isSystemInDarkTheme() - var showOffset by remember { mutableStateOf(false) } - var perListExpanded by remember { mutableStateOf(false) } - var listPickerId by remember { mutableStateOf(null) } - val lists = state.lists.filter { it.id > 0L } - - CollapsingScaffold(title = stringResource(R.string.settings_section_reminders), onBack = onBack) { - GroupedRow( - title = stringResource(R.string.settings_reminders), - summary = stringResource(R.string.settings_reminders_hint), - position = Position.Top, - trailing = { Switch(checked = state.settings.remindersEnabled, onCheckedChange = toggleReminders) }, - onClick = { toggleReminders(!state.settings.remindersEnabled) }, - ) - GroupedRow( - title = stringResource(R.string.settings_default_reminder), - summary = reminderLeadTimeLabel(state.settings.reminderLeadMinutes), - position = if (exactAlarmRow) Position.Middle else Position.Bottom, - onClick = { showOffset = true }, - ) - if (exactAlarmRow) { - GroupedRow( - title = stringResource(R.string.settings_exact_alarms), - summary = stringResource( - if (canExact) R.string.settings_exact_alarms_allowed - else R.string.settings_exact_alarms_blocked, - ), - position = Position.Bottom, - onClick = if (canExact) null else ({ context.openExactAlarmSettings() }), - ) - } - - // Per-list overrides: the whole section folds behind one header. Each - // list may keep, drop, or replace the global default for its tasks. - if (lists.isNotEmpty()) { - Spacer(Modifier.height(24.dp)) - // The header and the list rows form one connected run: the header - // opens its bottom (Top) when expanded, and the last list rounds it off. - GroupedRow( - title = stringResource(R.string.settings_list_reminders_title), - summary = stringResource(R.string.settings_list_reminders_hint), - position = if (perListExpanded) Position.Top else Position.Alone, - trailing = { - Icon( - imageVector = if (perListExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - onClick = { perListExpanded = !perListExpanded }, - ) - AnimatedVisibility( - visible = perListExpanded, - enter = expandEnter(), - exit = collapseExit(), - ) { - Column { - lists.forEachIndexed { index, list -> - GroupedRow( - title = list.name, - summary = listOverrideSummary( - listOverrideChoice(state, list.id), - state.settings.reminderLeadMinutes, - ), - // Every list is mid-run under the header; the last rounds off. - position = if (index == lists.lastIndex) Position.Bottom else Position.Middle, - leading = { - Icon(Icons.Rounded.Circle, contentDescription = null, tint = pastelize(list.color, dark)) - }, - onClick = { listPickerId = list.id }, - ) - } - } - } - } - } - - if (showOffset) { - ReminderLeadPicker( - title = stringResource(R.string.settings_default_reminder), - selected = ReminderOverride.Minutes(listOf(state.settings.reminderLeadMinutes)), - allowInherit = false, - allowNone = false, - onSelect = { if (it is ReminderOverride.Minutes) viewModel.setReminderLeadMinutes(it.minutes.first()) }, - onDismiss = { showOffset = false }, - ) - } - listPickerId?.let { id -> - ReminderLeadPicker( - title = state.lists.firstOrNull { it.id == id }?.name - ?: stringResource(R.string.settings_default_reminder), - selected = listOverrideChoice(state, id), - allowInherit = true, - allowNone = true, - onSelect = { viewModel.setListReminderOverride(id, it) }, - onDismiss = { listPickerId = null }, - ) - } -} - -/** The stored override for [listId], as a picker choice (absent → inherit). */ -private fun listOverrideChoice(state: SettingsUiState, listId: Long): ReminderOverride = - state.settings.perListReminderOverride.reminderOverrideFor(listId) - -/** Row summary for a list: its override, or the inherited global default. */ -@Composable -private fun listOverrideSummary(choice: ReminderOverride, globalDefault: Int): String = when (choice) { - ReminderOverride.Inherit -> - stringResource(R.string.settings_list_reminder_inherits, reminderLeadTimeLabel(globalDefault)) - ReminderOverride.None -> stringResource(R.string.reminder_none) - is ReminderOverride.Minutes -> reminderLeadTimeLabel(choice.minutes.first()) -} - -// --------------------------------------------------------------------------- -// About + shared building blocks -// --------------------------------------------------------------------------- - -@Composable -private fun AppLogo() { - Box( - modifier = Modifier - .size(72.dp) - .clip(RoundedCornerShape(20.dp)) - .background(colorResource(R.color.ic_launcher_background)), - contentAlignment = Alignment.Center, - ) { - Image( - painter = painterResource(R.drawable.ic_launcher_foreground), - contentDescription = stringResource(R.string.settings_about_logo_desc), - modifier = Modifier.requiredSize(108.dp), - ) - } -} - -@Composable -private fun AppVersionText() { - val context = LocalContext.current - val versionName = remember { - runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName } - .getOrNull() ?: "—" - } - Text( - text = stringResource(R.string.settings_about_version, versionName), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), - ) -} - -/** Leading circular icon chip; colours come from the M3 scheme via a token pair. */ -@Composable -private fun CategoryIcon(icon: ImageVector, accent: ChipAccent) { - val scheme = MaterialTheme.colorScheme - val (background, iconColor) = when (accent) { - ChipAccent.Neutral -> scheme.surfaceContainerHighest to scheme.onSurfaceVariant - ChipAccent.Primary -> scheme.primaryContainer to scheme.onPrimaryContainer - ChipAccent.Tertiary -> scheme.tertiaryContainer to scheme.onTertiaryContainer - } - Box( - modifier = Modifier.size(40.dp).clip(CircleShape).background(background), - contentAlignment = Alignment.Center, - ) { - Icon(icon, contentDescription = null, tint = iconColor, modifier = Modifier.size(22.dp)) - } -} - -@Composable -private fun defaultListLabel(state: SettingsUiState): String = - state.lists.firstOrNull { it.id == state.settings.defaultListId }?.name - ?: stringResource(R.string.settings_default_list_first) - -private fun openUrl(context: Context, url: String) { - runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) } -} - -private fun themeLabel(mode: ThemeMode): Int = when (mode) { - ThemeMode.SYSTEM -> R.string.settings_theme_system - ThemeMode.LIGHT -> R.string.settings_theme_light - ThemeMode.DARK -> R.string.settings_theme_dark -} - -private fun formFieldLabel(field: TaskFormField): Int = when (field) { - TaskFormField.Description -> R.string.field_description - TaskFormField.Priority -> R.string.edit_priority_label - TaskFormField.Progress -> R.string.edit_progress_label - TaskFormField.Parent -> R.string.edit_parent_label - TaskFormField.Reminder -> R.string.edit_reminder_label -} - -private fun formFieldIcon(field: TaskFormField): ImageVector = when (field) { - TaskFormField.Description -> Icons.AutoMirrored.Rounded.Notes - TaskFormField.Priority -> Icons.Rounded.Flag - TaskFormField.Progress -> Icons.Rounded.Percent - TaskFormField.Parent -> Icons.Rounded.AccountTree - TaskFormField.Reminder -> Icons.Default.Notifications -} - -/** - * Exact-alarm control is only meaningful on API 31-32 (SCHEDULE_EXACT_ALARM, - * revocable). On 33+ the app holds USE_EXACT_ALARM (always granted), so the row - * would be a permanent no-op — hide it there. - */ -private fun showExactAlarmRow(): Boolean = - Build.VERSION.SDK_INT in Build.VERSION_CODES.S..Build.VERSION_CODES.S_V2 - -@Composable -private fun rememberExactAlarmAllowed(context: Context): Boolean { - var allowed by remember { - mutableStateOf( - Build.VERSION.SDK_INT < Build.VERSION_CODES.S || - context.getSystemService(AlarmManager::class.java).canScheduleExactAlarms(), - ) - } - OnResume { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - allowed = context.getSystemService(AlarmManager::class.java).canScheduleExactAlarms() - } - } - return allowed -} - -private fun Context.openExactAlarmSettings() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - runCatching { - startActivity( - Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).setData("package:$packageName".toUri()), - ) - } - } -} diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/TaskFormScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/TaskFormScreen.kt new file mode 100644 index 0000000..1eb7924 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/TaskFormScreen.kt @@ -0,0 +1,145 @@ +package de.jeanlucmakiola.agendula.ui.settings + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Notes +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.rounded.AccountTree +import androidx.compose.material.icons.rounded.Circle +import androidx.compose.material.icons.rounded.Flag +import androidx.compose.material.icons.rounded.Percent +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.jeanlucmakiola.agendula.R +import de.jeanlucmakiola.agendula.domain.TaskFormField +import de.jeanlucmakiola.floret.components.CollapsingScaffold +import de.jeanlucmakiola.floret.components.GroupedRow +import de.jeanlucmakiola.floret.components.OptionPicker +import de.jeanlucmakiola.floret.components.Position +import de.jeanlucmakiola.floret.components.pastelize +import de.jeanlucmakiola.floret.components.positionOf + +/** What a new task's edit form starts out as: its fields, its list, and the add affordances. */ +@Composable +internal fun TaskFormScreen( + state: SettingsUiState, + viewModel: SettingsViewModel, + onBack: () -> Unit, +) { + var showDefaultList by remember { mutableStateOf(false) } + + CollapsingScaffold(title = stringResource(R.string.settings_section_task_form), onBack = onBack) { + Text( + text = stringResource(R.string.settings_form_fields_hint), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + Spacer(Modifier.height(8.dp)) + val fields = TaskFormField.entries + fields.forEachIndexed { index, field -> + val checked = field in state.settings.defaultEditFields + GroupedRow( + title = stringResource(formFieldLabel(field)), + position = positionOf(index, fields.size), + leading = { + Icon( + imageVector = formFieldIcon(field), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + trailing = { + Switch(checked = checked, onCheckedChange = { viewModel.setFormFieldDefault(field, it) }) + }, + onClick = { viewModel.setFormFieldDefault(field, !checked) }, + ) + } + + Spacer(Modifier.height(24.dp)) + GroupedRow( + title = stringResource(R.string.settings_default_list), + summary = defaultListLabel(state), + position = Position.Top, + onClick = { showDefaultList = true }, + ) + GroupedRow( + title = stringResource(R.string.settings_add_subtask_row), + summary = stringResource(R.string.settings_add_subtask_row_hint), + position = Position.Middle, + trailing = { + Switch( + checked = state.settings.showAddSubtaskRow, + onCheckedChange = viewModel::setShowAddSubtaskRow, + ) + }, + onClick = { viewModel.setShowAddSubtaskRow(!state.settings.showAddSubtaskRow) }, + ) + GroupedRow( + title = stringResource(R.string.settings_bottom_add_bar), + summary = stringResource(R.string.settings_bottom_add_bar_hint), + position = Position.Bottom, + trailing = { + Switch( + checked = state.settings.bottomAddBar, + onCheckedChange = viewModel::setBottomAddBar, + ) + }, + onClick = { viewModel.setBottomAddBar(!state.settings.bottomAddBar) }, + ) + } + + if (showDefaultList) { + val dark = isSystemInDarkTheme() + // null = first available list; the rest are real list ids. + val options = remember(state.lists) { listOf(null) + state.lists.map { it.id } } + OptionPicker( + title = stringResource(R.string.settings_default_list), + options = options, + selected = state.settings.defaultListId, + label = { id -> state.lists.firstOrNull { it.id == id }?.name ?: stringResource(R.string.settings_default_list_first) }, + leading = { id -> + state.lists.firstOrNull { it.id == id }?.let { list -> + Icon(Icons.Rounded.Circle, contentDescription = null, tint = pastelize(list.color, dark)) + } + }, + onSelect = { viewModel.setDefaultList(it) }, + onDismiss = { showDefaultList = false }, + ) + } +} + +@Composable +private fun defaultListLabel(state: SettingsUiState): String = + state.lists.firstOrNull { it.id == state.settings.defaultListId }?.name + ?: stringResource(R.string.settings_default_list_first) + +private fun formFieldLabel(field: TaskFormField): Int = when (field) { + TaskFormField.Description -> R.string.field_description + TaskFormField.Priority -> R.string.edit_priority_label + TaskFormField.Progress -> R.string.edit_progress_label + TaskFormField.Parent -> R.string.edit_parent_label + TaskFormField.Reminder -> R.string.edit_reminder_label +} + +private fun formFieldIcon(field: TaskFormField): ImageVector = when (field) { + TaskFormField.Description -> Icons.AutoMirrored.Rounded.Notes + TaskFormField.Priority -> Icons.Rounded.Flag + TaskFormField.Progress -> Icons.Rounded.Percent + TaskFormField.Parent -> Icons.Rounded.AccountTree + TaskFormField.Reminder -> Icons.Default.Notifications +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9bf11b..1ef670c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -244,6 +244,9 @@ Settings + + Sync and data + About Appearance Theme and colour Task form diff --git a/floret-kit b/floret-kit index d7b7a54..1af2484 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit d7b7a542cd50e278d585cccbf2936b1ab66f799a +Subproject commit 1af24841ae8e670fb608253086f8c702d4f7ba09