Fix Android config save feedback and mirror watch reading layout
build / build (push) Successful in 3m8s
build / build (push) Successful in 3m8s
Gray out Save until settings are dirty, add a Cancel button that discards edits, reorder ReadingScreen to match the watch's build_content() (adds the missing Attitude/Outcome preview and the day-significance rank/count), and fix UI chrome to follow the app's own language setting instead of system locale (values-de/strings.xml + appStringResource()/localizedContext()).
This commit is contained in:
@@ -179,7 +179,30 @@ key-value persist API. The daily notification uses WorkManager (a
|
|||||||
self-rescheduling one-shot chain, not `AlarmManager`, since a "roughly
|
self-rescheduling one-shot chain, not `AlarmManager`, since a "roughly
|
||||||
this time daily" reminder doesn't need `AlarmManager`'s exact-alarm
|
this time daily" reminder doesn't need `AlarmManager`'s exact-alarm
|
||||||
permission burden) - the Android analog of `watch/src/c/notify.c`'s own
|
permission burden) - the Android analog of `watch/src/c/notify.c`'s own
|
||||||
daily-wakeup contract.
|
daily-wakeup contract. `ReadingScreen`'s layout mirrors
|
||||||
|
`watch/src/c/ui_report_window.c`'s `build_content()` section-for-section
|
||||||
|
(date → day significance, shown with its rank/count e.g. "Notable
|
||||||
|
(2/4)" → a Guidance section leading with an Attitude/Outcome card
|
||||||
|
preview, image only, then the day's top transit, then the guidance
|
||||||
|
paragraph → the full significant-transits list → the full Celtic Cross
|
||||||
|
spread), not the interpreter's own `--format html` layout, which orders
|
||||||
|
sections differently and is a separate, standalone rendering path.
|
||||||
|
|
||||||
|
The compiled-in `.lang` strings the JNI bridge already localizes (card
|
||||||
|
text, narrative, guidance) don't cover the Compose UI's own chrome
|
||||||
|
(nav labels, section headings, notification text) - those are ordinary
|
||||||
|
Android string resources, which by default resolve against the
|
||||||
|
*device's* system locale, not `DeckConfig.lang`. Since this app's
|
||||||
|
reading language is an explicit in-Settings choice independent of the
|
||||||
|
system locale (`android/app/src/main/res/values-de/strings.xml` holds
|
||||||
|
the German chrome strings), every chrome string is read through
|
||||||
|
`ui/AppStrings.kt`'s `appStringResource()` instead of Compose's own
|
||||||
|
`stringResource()` - it wraps the `Context` in a `Configuration`
|
||||||
|
forced to `LocalAppLanguage` (provided once, at the root, from
|
||||||
|
`ConfigRepository`'s `DeckConfig.lang`) via
|
||||||
|
`util/LocaleUtils.kt`'s `Context.localizedContext()`, the same helper
|
||||||
|
`ReadingNotificationWorker` uses for its own notification/channel text
|
||||||
|
outside Compose entirely.
|
||||||
|
|
||||||
**CI needs network access for this target specifically** - unlike the
|
**CI needs network access for this target specifically** - unlike the
|
||||||
CLI/Pebble build path above (deliberately zero network access needed
|
CLI/Pebble build path above (deliberately zero network access needed
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import de.ladkau.deckinadash.R
|
|||||||
import de.ladkau.deckinadash.data.ConfigRepository
|
import de.ladkau.deckinadash.data.ConfigRepository
|
||||||
import de.ladkau.deckinadash.data.ReadingDto
|
import de.ladkau.deckinadash.data.ReadingDto
|
||||||
import de.ladkau.deckinadash.data.ReadingRepository
|
import de.ladkau.deckinadash.data.ReadingRepository
|
||||||
|
import de.ladkau.deckinadash.util.localizedContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -62,13 +63,14 @@ class ReadingNotificationWorker(
|
|||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
postNotification(reading)
|
postNotification(reading, config.lang)
|
||||||
NotificationScheduler.reschedule(applicationContext, config)
|
NotificationScheduler.reschedule(applicationContext, config)
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postNotification(reading: ReadingDto) {
|
private fun postNotification(reading: ReadingDto, lang: String) {
|
||||||
ensureChannel()
|
val localized = applicationContext.localizedContext(lang)
|
||||||
|
ensureChannel(localized)
|
||||||
|
|
||||||
val body = reading.transits.firstOrNull()?.let { "${it.title}. ${it.narrative}" }
|
val body = reading.transits.firstOrNull()?.let { "${it.title}. ${it.narrative}" }
|
||||||
?: reading.guidance
|
?: reading.guidance
|
||||||
@@ -83,7 +85,7 @@ class ReadingNotificationWorker(
|
|||||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(
|
.setContentTitle(
|
||||||
applicationContext.getString(R.string.notification_title, reading.daySignificance.levelLabel),
|
localized.getString(R.string.notification_title, reading.daySignificance.levelLabel),
|
||||||
)
|
)
|
||||||
.setContentText(body)
|
.setContentText(body)
|
||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||||
@@ -101,15 +103,15 @@ class ReadingNotificationWorker(
|
|||||||
NotificationManagerCompat.from(applicationContext).notify(NOTIFICATION_ID, notification)
|
NotificationManagerCompat.from(applicationContext).notify(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureChannel() {
|
private fun ensureChannel(localized: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val manager = applicationContext.getSystemService(NotificationManager::class.java)
|
val manager = applicationContext.getSystemService(NotificationManager::class.java)
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
applicationContext.getString(R.string.notification_channel_name),
|
localized.getString(R.string.notification_channel_name),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT,
|
NotificationManager.IMPORTANCE_DEFAULT,
|
||||||
).apply {
|
).apply {
|
||||||
description = applicationContext.getString(R.string.notification_channel_description)
|
description = localized.getString(R.string.notification_channel_description)
|
||||||
}
|
}
|
||||||
manager.createNotificationChannel(channel)
|
manager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package de.ladkau.deckinadash.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import de.ladkau.deckinadash.util.localizedContext
|
||||||
|
|
||||||
|
/** The reading language selected in Settings (DeckConfig.lang) - provided
|
||||||
|
* once near the root (see DeckInADashApp) and read by every screen's
|
||||||
|
* appStringResource() call below instead of Compose's own stringResource(),
|
||||||
|
* which always follows the device's system locale rather than this
|
||||||
|
* in-app setting. */
|
||||||
|
val LocalAppLanguage = compositionLocalOf { "en" }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberLocalizedContext(): Context {
|
||||||
|
val lang = LocalAppLanguage.current
|
||||||
|
val base = LocalContext.current
|
||||||
|
return remember(base, lang) { base.localizedContext(lang) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun appStringResource(@StringRes id: Int): String = rememberLocalizedContext().getString(id)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun appStringResource(@StringRes id: Int, vararg formatArgs: Any): String =
|
||||||
|
rememberLocalizedContext().getString(id, *formatArgs)
|
||||||
@@ -15,9 +15,13 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -25,6 +29,8 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import de.ladkau.deckinadash.R
|
import de.ladkau.deckinadash.R
|
||||||
|
import de.ladkau.deckinadash.data.ConfigRepository
|
||||||
|
import de.ladkau.deckinadash.data.DeckConfig
|
||||||
import de.ladkau.deckinadash.ui.about.AboutScreen
|
import de.ladkau.deckinadash.ui.about.AboutScreen
|
||||||
import de.ladkau.deckinadash.ui.config.ConfigScreen
|
import de.ladkau.deckinadash.ui.config.ConfigScreen
|
||||||
import de.ladkau.deckinadash.ui.reading.ReadingScreen
|
import de.ladkau.deckinadash.ui.reading.ReadingScreen
|
||||||
@@ -46,24 +52,32 @@ fun DeckInADashApp() {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
||||||
|
|
||||||
|
// Provided here, at the root, so every screen's appStringResource()
|
||||||
|
// call below picks up the configured reading language rather than
|
||||||
|
// the device's system locale - see AppStrings.kt's own comment.
|
||||||
|
val context = LocalContext.current
|
||||||
|
val configRepository = remember { ConfigRepository(context) }
|
||||||
|
val appConfig by configRepository.config.collectAsState(initial = DeckConfig())
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalAppLanguage provides appConfig.lang) {
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
ModalDrawerSheet {
|
ModalDrawerSheet {
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
label = { Text(stringResource(R.string.nav_reading)) },
|
label = { Text(appStringResource(R.string.nav_reading)) },
|
||||||
selected = currentRoute == Destinations.READING,
|
selected = currentRoute == Destinations.READING,
|
||||||
onClick = { navigateTo(navController, Destinations.READING); scope.launch { drawerState.close() } },
|
onClick = { navigateTo(navController, Destinations.READING); scope.launch { drawerState.close() } },
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
)
|
)
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
label = { Text(stringResource(R.string.nav_settings)) },
|
label = { Text(appStringResource(R.string.nav_settings)) },
|
||||||
selected = currentRoute == Destinations.SETTINGS,
|
selected = currentRoute == Destinations.SETTINGS,
|
||||||
onClick = { navigateTo(navController, Destinations.SETTINGS); scope.launch { drawerState.close() } },
|
onClick = { navigateTo(navController, Destinations.SETTINGS); scope.launch { drawerState.close() } },
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
)
|
)
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
label = { Text(stringResource(R.string.nav_about)) },
|
label = { Text(appStringResource(R.string.nav_about)) },
|
||||||
selected = currentRoute == Destinations.ABOUT,
|
selected = currentRoute == Destinations.ABOUT,
|
||||||
onClick = { navigateTo(navController, Destinations.ABOUT); scope.launch { drawerState.close() } },
|
onClick = { navigateTo(navController, Destinations.ABOUT); scope.launch { drawerState.close() } },
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
@@ -74,10 +88,10 @@ fun DeckInADashApp() {
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.app_name)) },
|
title = { Text(appStringResource(R.string.app_name)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
IconButton(onClick = { scope.launch { drawerState.open() } }) {
|
||||||
Icon(Icons.Filled.Menu, contentDescription = stringResource(R.string.nav_settings))
|
Icon(Icons.Filled.Menu, contentDescription = appStringResource(R.string.nav_settings))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -96,6 +110,7 @@ fun DeckInADashApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateTo(navController: NavHostController, route: String) {
|
private fun navigateTo(navController: NavHostController, route: String) {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.ladkau.deckinadash.R
|
import de.ladkau.deckinadash.R
|
||||||
|
import de.ladkau.deckinadash.ui.appStringResource
|
||||||
|
|
||||||
/** Static Waite/Sepharial public-domain attribution, carried forward
|
/** Static Waite/Sepharial public-domain attribution, carried forward
|
||||||
* from the root CLAUDE.md's own "Licensing" section, since this app's
|
* from the root CLAUDE.md's own "Licensing" section, since this app's
|
||||||
@@ -17,7 +17,7 @@ import de.ladkau.deckinadash.R
|
|||||||
@Composable
|
@Composable
|
||||||
fun AboutScreen(modifier: Modifier = Modifier) {
|
fun AboutScreen(modifier: Modifier = Modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.about_body),
|
text = appStringResource(R.string.about_body),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
|||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.SegmentedButton
|
import androidx.compose.material3.SegmentedButton
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
@@ -38,13 +39,13 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.ladkau.deckinadash.R
|
import de.ladkau.deckinadash.R
|
||||||
import de.ladkau.deckinadash.data.Gender
|
import de.ladkau.deckinadash.data.Gender
|
||||||
|
import de.ladkau.deckinadash.ui.appStringResource
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ fun ConfigScreen(
|
|||||||
viewModel: ConfigViewModel = viewModel(),
|
viewModel: ConfigViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val config by viewModel.config.collectAsState()
|
val config by viewModel.config.collectAsState()
|
||||||
|
val isDirty by viewModel.isDirty.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var showDatePicker by remember { mutableStateOf(false) }
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
@@ -74,17 +76,17 @@ fun ConfigScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.config_birth_date), style = MaterialTheme.typography.titleSmall)
|
Text(appStringResource(R.string.config_birth_date), style = MaterialTheme.typography.titleSmall)
|
||||||
Button(onClick = { showDatePicker = true }) {
|
Button(onClick = { showDatePicker = true }) {
|
||||||
val label = if (config.birthYear > 0) {
|
val label = if (config.birthYear > 0) {
|
||||||
"%04d-%02d-%02d".format(config.birthYear, config.birthMonth, config.birthDay)
|
"%04d-%02d-%02d".format(config.birthYear, config.birthMonth, config.birthDay)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.config_birth_date)
|
appStringResource(R.string.config_birth_date)
|
||||||
}
|
}
|
||||||
Text(label)
|
Text(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(stringResource(R.string.config_birth_time), style = MaterialTheme.typography.titleSmall)
|
Text(appStringResource(R.string.config_birth_time), style = MaterialTheme.typography.titleSmall)
|
||||||
Button(onClick = { showBirthTimePicker = true }) {
|
Button(onClick = { showBirthTimePicker = true }) {
|
||||||
Text("%02d:%02d".format(config.birthHour, config.birthMinute))
|
Text("%02d:%02d".format(config.birthHour, config.birthMinute))
|
||||||
}
|
}
|
||||||
@@ -94,7 +96,7 @@ fun ConfigScreen(
|
|||||||
onValueChange = { text ->
|
onValueChange = { text ->
|
||||||
text.toDoubleOrNull()?.let { value -> viewModel.update { it.copy(utcOffsetHours = value) } }
|
text.toDoubleOrNull()?.let { value -> viewModel.update { it.copy(utcOffsetHours = value) } }
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.config_utc_offset)) },
|
label = { Text(appStringResource(R.string.config_utc_offset)) },
|
||||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -102,7 +104,7 @@ fun ConfigScreen(
|
|||||||
onValueChange = { text ->
|
onValueChange = { text ->
|
||||||
text.toDoubleOrNull()?.let { value -> viewModel.update { it.copy(latitude = value) } }
|
text.toDoubleOrNull()?.let { value -> viewModel.update { it.copy(latitude = value) } }
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.config_latitude)) },
|
label = { Text(appStringResource(R.string.config_latitude)) },
|
||||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -110,15 +112,15 @@ fun ConfigScreen(
|
|||||||
onValueChange = { text ->
|
onValueChange = { text ->
|
||||||
text.toDoubleOrNull()?.let { value -> viewModel.update { it.copy(longitude = value) } }
|
text.toDoubleOrNull()?.let { value -> viewModel.update { it.copy(longitude = value) } }
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.config_longitude)) },
|
label = { Text(appStringResource(R.string.config_longitude)) },
|
||||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(stringResource(R.string.config_gender), style = MaterialTheme.typography.titleSmall)
|
Text(appStringResource(R.string.config_gender), style = MaterialTheme.typography.titleSmall)
|
||||||
val genderOptions = listOf(
|
val genderOptions = listOf(
|
||||||
Gender.UNSPECIFIED to stringResource(R.string.config_gender_unspecified),
|
Gender.UNSPECIFIED to appStringResource(R.string.config_gender_unspecified),
|
||||||
Gender.MALE to stringResource(R.string.config_gender_male),
|
Gender.MALE to appStringResource(R.string.config_gender_male),
|
||||||
Gender.FEMALE to stringResource(R.string.config_gender_female),
|
Gender.FEMALE to appStringResource(R.string.config_gender_female),
|
||||||
)
|
)
|
||||||
SingleChoiceSegmentedButtonRow {
|
SingleChoiceSegmentedButtonRow {
|
||||||
genderOptions.forEachIndexed { index, (gender, label) ->
|
genderOptions.forEachIndexed { index, (gender, label) ->
|
||||||
@@ -136,7 +138,7 @@ fun ConfigScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Text(stringResource(R.string.config_notify_enabled), style = MaterialTheme.typography.titleSmall)
|
Text(appStringResource(R.string.config_notify_enabled), style = MaterialTheme.typography.titleSmall)
|
||||||
Switch(
|
Switch(
|
||||||
checked = config.notifyEnabled,
|
checked = config.notifyEnabled,
|
||||||
onCheckedChange = { enabled ->
|
onCheckedChange = { enabled ->
|
||||||
@@ -154,8 +156,13 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(onClick = { viewModel.save {} }) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text(stringResource(R.string.config_save))
|
OutlinedButton(onClick = { viewModel.cancel() }, enabled = isDirty) {
|
||||||
|
Text(appStringResource(R.string.config_cancel))
|
||||||
|
}
|
||||||
|
Button(onClick = { viewModel.save {} }, enabled = isDirty) {
|
||||||
|
Text(appStringResource(R.string.config_save))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +186,7 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
showDatePicker = false
|
showDatePicker = false
|
||||||
}) { Text(stringResource(R.string.config_save)) }
|
}) { Text(appStringResource(R.string.config_save)) }
|
||||||
},
|
},
|
||||||
) { DatePicker(state = datePickerState) }
|
) { DatePicker(state = datePickerState) }
|
||||||
}
|
}
|
||||||
@@ -222,7 +229,7 @@ private fun TimePickerDialogContent(
|
|||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { onConfirm(state.hour, state.minute) }) {
|
TextButton(onClick = { onConfirm(state.hour, state.minute) }) {
|
||||||
Text(stringResource(R.string.config_save))
|
Text(appStringResource(R.string.config_save))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = { TimePicker(state = state) },
|
text = { TimePicker(state = state) },
|
||||||
@@ -239,7 +246,7 @@ private fun LanguageDropdown(selected: String, onSelected: (String) -> Unit) {
|
|||||||
value = selected,
|
value = selected,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text(stringResource(R.string.config_language)) },
|
label = { Text(appStringResource(R.string.config_language)) },
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
|
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import de.ladkau.deckinadash.data.ConfigRepository
|
|||||||
import de.ladkau.deckinadash.data.DeckConfig
|
import de.ladkau.deckinadash.data.DeckConfig
|
||||||
import de.ladkau.deckinadash.notify.NotificationScheduler
|
import de.ladkau.deckinadash.notify.NotificationScheduler
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ConfigViewModel(application: Application) : AndroidViewModel(application) {
|
class ConfigViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
@@ -22,9 +25,20 @@ class ConfigViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
private val _config = MutableStateFlow(DeckConfig())
|
private val _config = MutableStateFlow(DeckConfig())
|
||||||
val config: StateFlow<DeckConfig> = _config.asStateFlow()
|
val config: StateFlow<DeckConfig> = _config.asStateFlow()
|
||||||
|
|
||||||
|
// The last value actually persisted (or loaded from DataStore) -
|
||||||
|
// compared against the draft above to drive the Save/Cancel buttons'
|
||||||
|
// enabled state, so Save visibly grays out once there's nothing to
|
||||||
|
// save rather than always looking clickable.
|
||||||
|
private val _savedConfig = MutableStateFlow(DeckConfig())
|
||||||
|
|
||||||
|
val isDirty: StateFlow<Boolean> = combine(_config, _savedConfig) { draft, saved -> draft != saved }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_config.value = configRepository.config.first()
|
val loaded = configRepository.config.first()
|
||||||
|
_config.value = loaded
|
||||||
|
_savedConfig.value = loaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +46,11 @@ class ConfigViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
_config.value = transform(_config.value)
|
_config.value = transform(_config.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Discards the current draft, reverting it to the last persisted value. */
|
||||||
|
fun cancel() {
|
||||||
|
_config.value = _savedConfig.value
|
||||||
|
}
|
||||||
|
|
||||||
/** Persists the current draft and reschedules the daily notification -
|
/** Persists the current draft and reschedules the daily notification -
|
||||||
* the other of NotificationScheduler.reschedule()'s two required call
|
* the other of NotificationScheduler.reschedule()'s two required call
|
||||||
* sites (see its own doc comment; the first is app launch). */
|
* sites (see its own doc comment; the first is app launch). */
|
||||||
@@ -39,6 +58,8 @@ class ConfigViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val toSave = _config.value.copy(configured = true)
|
val toSave = _config.value.copy(configured = true)
|
||||||
configRepository.save(toSave)
|
configRepository.save(toSave)
|
||||||
|
_config.value = toSave
|
||||||
|
_savedConfig.value = toSave
|
||||||
NotificationScheduler.reschedule(getApplication(), toSave)
|
NotificationScheduler.reschedule(getApplication(), toSave)
|
||||||
onSaved()
|
onSaved()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import androidx.compose.foundation.Image
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
@@ -23,13 +25,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.ladkau.deckinadash.R
|
import de.ladkau.deckinadash.R
|
||||||
import de.ladkau.deckinadash.data.ReadingDto
|
import de.ladkau.deckinadash.data.ReadingDto
|
||||||
import de.ladkau.deckinadash.data.SpreadPositionDto
|
import de.ladkau.deckinadash.data.SpreadPositionDto
|
||||||
import de.ladkau.deckinadash.data.TransitDto
|
import de.ladkau.deckinadash.data.TransitDto
|
||||||
|
import de.ladkau.deckinadash.ui.appStringResource
|
||||||
import de.ladkau.deckinadash.ui.cardDrawableRes
|
import de.ladkau.deckinadash.ui.cardDrawableRes
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -42,60 +44,93 @@ fun ReadingScreen(
|
|||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
when (val state = uiState) {
|
when (val state = uiState) {
|
||||||
is ReadingUiState.Loading -> CenteredMessage(stringResource(R.string.reading_loading))
|
is ReadingUiState.Loading -> CenteredMessage(appStringResource(R.string.reading_loading), loading = true)
|
||||||
is ReadingUiState.NotConfigured -> Column(
|
is ReadingUiState.NotConfigured -> Column(
|
||||||
modifier = Modifier.wrapContentSize(Alignment.Center).padding(24.dp),
|
modifier = Modifier.wrapContentSize(Alignment.Center).padding(24.dp),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.reading_not_configured))
|
Text(appStringResource(R.string.reading_not_configured))
|
||||||
Button(onClick = onNavigateToSettings, modifier = Modifier.padding(top = 16.dp)) {
|
Button(onClick = onNavigateToSettings, modifier = Modifier.padding(top = 16.dp)) {
|
||||||
Text(stringResource(R.string.nav_settings))
|
Text(appStringResource(R.string.nav_settings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ReadingUiState.Error -> CenteredMessage(stringResource(R.string.reading_error))
|
is ReadingUiState.Error -> CenteredMessage(appStringResource(R.string.reading_error))
|
||||||
is ReadingUiState.Content -> ReadingContent(state.reading)
|
is ReadingUiState.Content -> ReadingContent(state.reading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CenteredMessage(text: String) {
|
private fun CenteredMessage(text: String, loading: Boolean = false) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
if (text == stringResource(R.string.reading_loading)) {
|
if (loading) {
|
||||||
CircularProgressIndicator(modifier = Modifier.padding(bottom = 16.dp))
|
CircularProgressIndicator(modifier = Modifier.padding(bottom = 16.dp))
|
||||||
}
|
}
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Single scrollable page, same content order as
|
/** Single scrollable page, mirroring watch/src/c/ui_report_window.c's
|
||||||
* watch/src/c/ui_report_window.c's build_content(): date, day
|
* build_content() section-for-section: date; day significance (with its
|
||||||
* significance, top transits+narratives, full Celtic Cross spread,
|
* rank/count, e.g. "Notable (2/4)"); a "Guidance" section that leads with
|
||||||
* guidance last. */
|
* the Attitude/Outcome card preview (image only, no caption - same
|
||||||
|
* reasoning as that file's own comment) followed by the day's single
|
||||||
|
* top transit and the guidance paragraph; the full list of significant
|
||||||
|
* transits; and finally the full Celtic Cross spread. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReadingContent(reading: ReadingDto) {
|
private fun ReadingContent(reading: ReadingDto) {
|
||||||
|
val attitude = reading.spread.firstOrNull { it.positionSlug == "attitude" }
|
||||||
|
val outcome = reading.spread.firstOrNull { it.positionSlug == "outcome" }
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Text(reading.date, style = MaterialTheme.typography.labelLarge)
|
Text(reading.date, style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
Text(
|
Text(
|
||||||
reading.daySignificance.levelLabel,
|
appStringResource(R.string.reading_day_significance_heading),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${reading.daySignificance.levelLabel} (${reading.daySignificance.rank}/${reading.daySignificance.count})",
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reading.transits.isNotEmpty()) {
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.reading_transits_heading),
|
appStringResource(R.string.reading_guidance_heading),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (attitude != null && outcome != null) {
|
||||||
|
item { AttitudeOutcomeRow(attitude, outcome) }
|
||||||
|
}
|
||||||
|
reading.transits.firstOrNull()?.let { topTransit ->
|
||||||
|
item { TransitItem(topTransit) }
|
||||||
|
}
|
||||||
|
item { Text(reading.guidance, style = MaterialTheme.typography.bodyMedium) }
|
||||||
|
|
||||||
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
appStringResource(R.string.reading_transits_heading),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (reading.transits.isEmpty()) {
|
||||||
|
item { Text(appStringResource(R.string.reading_no_notable_transits)) }
|
||||||
|
} else {
|
||||||
items(reading.transits) { transit -> TransitItem(transit) }
|
items(reading.transits) { transit -> TransitItem(transit) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,22 +138,35 @@ private fun ReadingContent(reading: ReadingDto) {
|
|||||||
|
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.reading_spread_heading),
|
appStringResource(R.string.reading_spread_heading),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(reading.spread) { position -> SpreadItem(position) }
|
items(reading.spread) { position -> SpreadItem(position) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
@Composable
|
||||||
|
private fun AttitudeOutcomeRow(attitude: SpreadPositionDto, outcome: SpreadPositionDto) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
AttitudeOutcomeImage(attitude)
|
||||||
|
AttitudeOutcomeImage(outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item {
|
@Composable
|
||||||
Text(
|
private fun AttitudeOutcomeImage(position: SpreadPositionDto) {
|
||||||
stringResource(R.string.reading_guidance_heading),
|
val context = LocalContext.current
|
||||||
style = MaterialTheme.typography.titleMedium,
|
Image(
|
||||||
|
painter = painterResource(cardDrawableRes(context, position.cardSlug)),
|
||||||
|
contentDescription = position.cardName,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(140.dp)
|
||||||
|
.graphicsLayer { rotationZ = if (position.reversed) 180f else 0f },
|
||||||
)
|
)
|
||||||
Text(reading.guidance, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -145,7 +193,7 @@ private fun SpreadItem(position: SpreadPositionDto) {
|
|||||||
.graphicsLayer { rotationZ = if (position.reversed) 180f else 0f },
|
.graphicsLayer { rotationZ = if (position.reversed) 180f else 0f },
|
||||||
)
|
)
|
||||||
val cardTitle = if (position.reversed) {
|
val cardTitle = if (position.reversed) {
|
||||||
"${position.cardName} ${stringResource(R.string.reading_reversed)}"
|
"${position.cardName} ${appStringResource(R.string.reading_reversed)}"
|
||||||
} else {
|
} else {
|
||||||
position.cardName
|
position.cardName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package de.ladkau.deckinadash.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Context whose string resources are resolved for [lang],
|
||||||
|
* independent of the device's system locale. DeckConfig.lang is a
|
||||||
|
* user-chosen reading language (see ConfigScreen's language dropdown),
|
||||||
|
* separate from whatever locale the phone itself is set to - the same way
|
||||||
|
* the native engine's i18n_get() already renders card/narrative/guidance
|
||||||
|
* text in that language regardless of system locale. Without this, Compose's
|
||||||
|
* own stringResource()/Context.getString() would resolve UI chrome text
|
||||||
|
* (headings, notification text) against the system locale's resources
|
||||||
|
* instead, which for a language with no values-<lang>/ directory at all
|
||||||
|
* would render as English even when the reading itself is in German.
|
||||||
|
*/
|
||||||
|
fun Context.localizedContext(lang: String): Context {
|
||||||
|
val config = Configuration(resources.configuration)
|
||||||
|
config.setLocale(Locale.forLanguageTag(lang))
|
||||||
|
return createConfigurationContext(config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- German translation of the UI chrome text in ../values/strings.xml -
|
||||||
|
keys must match exactly, same convention as engine/i18n/de.lang.
|
||||||
|
Read either via the system locale (a German-locale device) or,
|
||||||
|
primarily, via LocaleUtils.kt's localizedContext(lang) so the app's
|
||||||
|
own in-Settings reading-language choice (independent of the device's
|
||||||
|
system locale) picks these up - see ui/AppStrings.kt. -->
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Deck in a Dash</string>
|
||||||
|
|
||||||
|
<string name="nav_reading">Heutige Lesung</string>
|
||||||
|
<string name="nav_settings">Einstellungen</string>
|
||||||
|
<string name="nav_about">Über</string>
|
||||||
|
|
||||||
|
<string name="notification_channel_name">Tägliche Lesung</string>
|
||||||
|
<string name="notification_channel_description">Eine tägliche Benachrichtigung mit deiner Tarot- und Astrologie-Lesung</string>
|
||||||
|
<string name="notification_title">Heutige Lesung: %1$s</string>
|
||||||
|
|
||||||
|
<string name="config_title">Einstellungen</string>
|
||||||
|
<string name="config_birth_date">Geburtsdatum</string>
|
||||||
|
<string name="config_birth_time">Geburtszeit</string>
|
||||||
|
<string name="config_utc_offset">UTC-Versatz (Stunden)</string>
|
||||||
|
<string name="config_latitude">Breitengrad</string>
|
||||||
|
<string name="config_longitude">Längengrad</string>
|
||||||
|
<string name="config_gender">Lesung für</string>
|
||||||
|
<string name="config_gender_unspecified">Nicht angegeben</string>
|
||||||
|
<string name="config_gender_male">Männlich</string>
|
||||||
|
<string name="config_gender_female">Weiblich</string>
|
||||||
|
<string name="config_language">Sprache</string>
|
||||||
|
<string name="config_notify_enabled">Tägliche Benachrichtigung</string>
|
||||||
|
<string name="config_notify_time">Benachrichtigungszeit</string>
|
||||||
|
<string name="config_save">Speichern</string>
|
||||||
|
<string name="config_cancel">Verwerfen</string>
|
||||||
|
|
||||||
|
<string name="reading_not_configured">Trage deine Geburtsdaten in den Einstellungen ein, um die heutige Lesung zu sehen.</string>
|
||||||
|
<string name="reading_loading">Berechne die heutige Lesung…</string>
|
||||||
|
<string name="reading_error">Die heutige Lesung konnte nicht berechnet werden.</string>
|
||||||
|
<string name="reading_day_significance_heading">Bedeutung des Tages</string>
|
||||||
|
<string name="reading_guidance_heading">Rat für heute</string>
|
||||||
|
<string name="reading_transits_heading">Wichtige Transits</string>
|
||||||
|
<string name="reading_no_notable_transits">Heute keine nennenswerten Transits.</string>
|
||||||
|
<string name="reading_spread_heading">Keltisches Kreuz</string>
|
||||||
|
<string name="reading_reversed">(Umgekehrt)</string>
|
||||||
|
|
||||||
|
<string name="about_body">Deck in a Dash zeigt eine tägliche, persönliche Tarot-Lesung (Keltisches Kreuz) und Astrologie-Deutung (Geburtshoroskop + Transite).\n\nKarten- und Legetext basiert auf A. E. Waites The Pictorial Key to the Tarot (1911, gemeinfrei). Der Transit-Text basiert auf Sepharials Transits and Planetary Periods (1920, gemeinfrei).</string>
|
||||||
|
</resources>
|
||||||
@@ -24,12 +24,15 @@
|
|||||||
<string name="config_notify_enabled">Daily notification</string>
|
<string name="config_notify_enabled">Daily notification</string>
|
||||||
<string name="config_notify_time">Notification time</string>
|
<string name="config_notify_time">Notification time</string>
|
||||||
<string name="config_save">Save</string>
|
<string name="config_save">Save</string>
|
||||||
|
<string name="config_cancel">Cancel</string>
|
||||||
|
|
||||||
<string name="reading_not_configured">Set your birth details in Settings to see today\'s reading.</string>
|
<string name="reading_not_configured">Set your birth details in Settings to see today\'s reading.</string>
|
||||||
<string name="reading_loading">Computing today\'s reading…</string>
|
<string name="reading_loading">Computing today\'s reading…</string>
|
||||||
<string name="reading_error">Could not compute today\'s reading.</string>
|
<string name="reading_error">Could not compute today\'s reading.</string>
|
||||||
|
<string name="reading_day_significance_heading">Day Significance</string>
|
||||||
<string name="reading_guidance_heading">Guidance</string>
|
<string name="reading_guidance_heading">Guidance</string>
|
||||||
<string name="reading_transits_heading">Significant transits</string>
|
<string name="reading_transits_heading">Significant Transits</string>
|
||||||
|
<string name="reading_no_notable_transits">No notable transits today.</string>
|
||||||
<string name="reading_spread_heading">Celtic Cross</string>
|
<string name="reading_spread_heading">Celtic Cross</string>
|
||||||
<string name="reading_reversed">(Reversed)</string>
|
<string name="reading_reversed">(Reversed)</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user