Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faa3e3817f | ||
|
|
71591f617a | ||
|
|
eea3a8b917 | ||
|
|
a454c3685e |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -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
|
||||
|
||||
@@ -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 & 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 & 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 & 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 & 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>
|
||||
|
||||
+1
-1
Submodule floret-kit updated: 0b44d353d2...4858e00dfa
Reference in New Issue
Block a user