Add support for different calendar systems, add secondary calendar

This commit is contained in:
MM20
2026-03-06 19:36:58 +01:00
parent f281efb5d7
commit 77eb927ab2
29 changed files with 504 additions and 139 deletions

View File

@@ -1,11 +1,13 @@
package de.mm20.launcher2.ui.base
import android.icu.util.Calendar
import android.icu.util.LocaleData
import android.icu.util.ULocale
import android.text.format.DateFormat
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@@ -17,6 +19,8 @@ import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.preferences.ui.LocaleSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.ui.component.ProvideIconShape
import de.mm20.launcher2.ui.locals.LocalCalendarSystemIds
import de.mm20.launcher2.ui.locals.LocalCalendarSystems
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.locals.LocalMeasurementSystem
@@ -82,13 +86,41 @@ fun ProvideSettings(
}.distinctUntilChanged()
}.collectAsState(null)
val calendarIds by remember {
localeSettings.primaryCalendar.combine(localeSettings.secondaryCalendar) { p, s ->
listOf(p, s)
}
}.collectAsState(listOf(null, null))
val calendars by remember {
derivedStateOf {
val p = calendarIds.first()
val s = calendarIds.last()
val primary = if (p == null) {
Calendar.getInstance(ULocale.getDefault())
} else {
Calendar.getInstance(ULocale.getDefault().setKeywordValue("calendar", p))
}
val secondary = if (s == null) {
null
} else {
Calendar.getInstance(ULocale.getDefault().setKeywordValue("calendar", s))
}
listOfNotNull(
primary, secondary
)
}
}
if (timeFormat == null || measurementSystem == null) return
CompositionLocalProvider(
LocalFavoritesEnabled provides favoritesEnabled,
LocalGridSettings provides gridSettings,
LocalTimeFormat provides timeFormat!!,
LocalMeasurementSystem provides measurementSystem!!
LocalMeasurementSystem provides measurementSystem!!,
LocalCalendarSystems provides calendars,
LocalCalendarSystemIds provides calendarIds,
) {
ProvideIconShape(iconShape) {
content()

View File

@@ -1,6 +1,10 @@
package de.mm20.launcher2.ui.launcher.search.calendar
import android.content.Context
import android.icu.text.DateFormat
import android.icu.text.DateIntervalFormat
import android.icu.util.DateInterval
import android.icu.util.ULocale
import android.text.Html
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedContent
@@ -18,6 +22,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -46,10 +51,12 @@ import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.locals.LocalCalendarSystemIds
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.locals.LocalTimeFormat
import de.mm20.launcher2.ui.utils.isTwentyFourHours
import palettes.TonalPalette
@Composable
@@ -147,15 +154,33 @@ fun CalendarItem(
painter = painterResource(R.drawable.schedule_24px),
contentDescription = null
)
Text(
modifier = Modifier
.sharedBounds(
rememberSharedContentState("date"),
this@AnimatedContent
),
text = calendar.formatTime(context, timeFormat),
style = MaterialTheme.typography.bodySmall
)
Column {
val primaryCalendar = LocalCalendarSystemIds.current.first()
val secondaryCalendar = LocalCalendarSystemIds.current.last()
Text(
modifier = Modifier
.sharedBounds(
rememberSharedContentState("date"),
this@AnimatedContent
),
text = calendar.formatTime(context, timeFormat, primaryCalendar),
style = MaterialTheme.typography.bodySmall
)
if (secondaryCalendar != null) {
Text(
modifier = modifier.padding(top = 2.dp),
text = calendar.formatTime(
context,
timeFormat,
secondaryCalendar
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
if (!calendar.description.isNullOrBlank()) {
Row(
@@ -258,10 +283,10 @@ fun CalendarItem(
val sheetManager = LocalBottomSheetManager.current
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_customize),
icon = R.drawable.tune_24px,
action = { sheetManager.showCustomizeSearchableModal(calendar) }
))
label = stringResource(R.string.menu_customize),
icon = R.drawable.tune_24px,
action = { sheetManager.showCustomizeSearchableModal(calendar) }
))
Toolbar(
leftActions = listOf(
@@ -321,7 +346,7 @@ fun CalendarItem(
rememberSharedContentState("date"),
this@AnimatedContent
),
text = calendar.getSummary(context, timeFormat),
text = calendar.getSummary(context, timeFormat, LocalCalendarSystemIds.current.first()),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -366,54 +391,47 @@ fun CalendarItemGridPopup(
private fun CalendarEvent.formatTime(
context: Context,
timeFormat: TimeFormat,
calendarSystem: String?,
): String {
val timeFormatFlag = when (timeFormat) {
TimeFormat.System -> 0
TimeFormat.TwelveHour -> DateUtils.FORMAT_12HOUR
TimeFormat.TwentyFourHour -> DateUtils.FORMAT_24HOUR
val locale = ULocale.getDefault().setKeywordValue("calendar", calendarSystem)
val time = if (timeFormat.isTwentyFourHours(context)) {
"HH:mm"
} else {
"hh:mm a"
}
val startTime = startTime
if (startTime == null || isTask) {
if (allDay) {
return DateUtils.formatDateRange(
context,
endTime,
endTime,
DateUtils.FORMAT_SHOW_DATE or timeFormatFlag
)
return DateFormat.getInstanceForSkeleton("EEE, MMMM d yyyy", locale)
.format(endTime)
}
return DateUtils.formatDateRange(
context,
endTime,
endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or timeFormatFlag
)
return DateFormat.getInstanceForSkeleton("EEE, MMMM d yyyy, $time", locale)
.format(endTime)
}
if (allDay) return DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or timeFormatFlag
)
return DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_WEEKDAY or timeFormatFlag
)
if (allDay) {
return DateIntervalFormat.getInstance("EEE, MMMM d yyyy", locale)
.format(DateInterval(startTime, endTime))
}
return DateIntervalFormat.getInstance("EEE, MMMM d yyyy, $time", locale)
.format(DateInterval(startTime, endTime))
}
private fun CalendarEvent.getSummary(
context: Context,
timeFormat: TimeFormat,
calendarSystem: String?,
): String {
val timeFormatFlag = when (timeFormat) {
TimeFormat.System -> 0
TimeFormat.TwelveHour -> DateUtils.FORMAT_12HOUR
TimeFormat.TwentyFourHour -> DateUtils.FORMAT_24HOUR
val locale = ULocale.getDefault().setKeywordValue("calendar", calendarSystem)
val time = if (timeFormat.isTwentyFourHours(context)) {
"HH:mm"
} else {
"hh:mm a"
}
val startTime = startTime
@@ -424,28 +442,22 @@ private fun CalendarEvent.getSummary(
return context.getString(R.string.task_due_today)
}
return context.getString(
R.string.task_due_time, DateUtils.formatDateTime(
context,
endTime,
DateUtils.FORMAT_SHOW_TIME or timeFormatFlag
)
R.string.task_due_time,
DateIntervalFormat.getInstance(time, locale)
.format(DateInterval(endTime, endTime))
)
}
if (allDay) {
return context.getString(
R.string.task_due_date, DateUtils.formatDateTime(
context,
endTime,
DateUtils.FORMAT_SHOW_DATE or timeFormatFlag
)
R.string.task_due_date,
DateFormat.getInstanceForSkeleton("EEE, MMMM d", locale)
.format(endTime)
)
}
return context.getString(
R.string.task_due_date, DateUtils.formatDateTime(
context,
endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or timeFormatFlag
)
R.string.task_due_date,
DateFormat.getInstanceForSkeleton("EEE, MMMM d, $time", locale)
.format(endTime)
)
}
@@ -455,28 +467,17 @@ private fun CalendarEvent.getSummary(
if (allDay) {
context.getString(R.string.calendar_event_allday)
} else {
DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_TIME or timeFormatFlag
)
DateIntervalFormat.getInstance(time, locale)
.format(DateInterval(startTime, endTime))
}
} else {
if (allDay) {
DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_DATE or timeFormatFlag
)
DateIntervalFormat.getInstance("MMMM d", locale)
.format(DateInterval(startTime, endTime))
} else {
DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE or timeFormatFlag
)
DateIntervalFormat.getInstance("MMMM d, $time", locale)
.format(DateInterval(startTime, endTime))
}
}
}

View File

@@ -1,6 +1,9 @@
package de.mm20.launcher2.ui.launcher.widgets.calendar
import android.content.Context
import android.icu.text.DateFormat
import android.icu.util.Calendar
import android.icu.util.ULocale
import android.text.format.DateUtils
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent
@@ -50,7 +53,9 @@ import de.mm20.launcher2.Quintuple
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
import de.mm20.launcher2.ui.locals.LocalCalendarSystems
import de.mm20.launcher2.widgets.CalendarWidget
import java.sql.Date
import java.time.LocalDate
import java.time.ZoneId
@@ -92,7 +97,7 @@ fun CalendarWidget(
var showDropdown by remember { mutableStateOf(false) }
TextButton(onClick = { showDropdown = true }) {
Text(
text = formatDay(context, selectedDate),
text = formatDay(context, selectedDate, LocalCalendarSystems.current[0]!!),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -120,7 +125,7 @@ fun CalendarWidget(
else -> MenuDefaults.middleItemShape
},
text = {
Text(formatDay(context, date))
Text(formatDay(context, date, LocalCalendarSystems.current[0]!!))
},
onClick = {
viewModel.selectDate(date)
@@ -316,15 +321,15 @@ private fun Info(
}
private fun formatDay(context: Context, day: LocalDate): String {
private fun formatDay(context: Context, day: LocalDate, calendar: Calendar): String {
val today = LocalDate.now()
return when {
today == day -> context.getString(R.string.date_today)
today.plusDays(1) == day -> context.getString(R.string.date_tomorrow)
else -> DateUtils.formatDateTime(
context,
day.atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY
)
else -> DateFormat.getInstanceForSkeleton(
calendar,
"E MMMM d",
ULocale.getDefault(),
).format(Date.from(day.atStartOfDay(ZoneId.systemDefault()).toInstant()))
}
}

View File

@@ -3,19 +3,23 @@ package de.mm20.launcher2.ui.launcher.widgets.clock.parts
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.icu.text.DateFormat
import android.icu.util.ULocale
import android.provider.CalendarContract
import android.text.format.DateFormat
import android.text.format.DateUtils
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.locals.LocalCalendarSystems
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.text.SimpleDateFormat
import java.util.*
class DatePartProvider : PartProvider {
@@ -26,8 +30,15 @@ class DatePartProvider : PartProvider {
@Composable
override fun Component(compactLayout: Boolean) {
val time = LocalTime.current
val date = Date(time)
val verticalLayout = !compactLayout
val context = LocalContext.current
val calendars = LocalCalendarSystems.current
val primaryCal = calendars.first()
val secondaryCal = calendars.getOrNull(1)
TextButton(
colors = ButtonDefaults.textButtonColors(
contentColor = LocalContentColor.current
@@ -44,26 +55,84 @@ class DatePartProvider : PartProvider {
context.tryStartActivity(intent)
}) {
if (verticalLayout) {
Text(
text = DateUtils.formatDateTime(
context,
time,
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR
),
style = MaterialTheme.typography.titleMedium
)
} else {
val line1Format = DateFormat.getBestDateTimePattern(Locale.getDefault(), "EEEE")
val line2Format =
DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM dd")
val format = SimpleDateFormat("$line1Format\n$line2Format")
Text(
text = format.format(time),
lineHeight = 1.2.em,
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Medium
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = DateFormat.getInstanceForSkeleton(
primaryCal,
"EEEE, MMMM d, yyyy",
ULocale.getDefault(),
).format(date),
style = MaterialTheme.typography.titleMedium
)
)
if (secondaryCal != null) {
Text(
text = DateFormat.getInstanceForSkeleton(
secondaryCal,
"MMMM dd, yyyy",
ULocale.getDefault(),
).format(date),
style = MaterialTheme.typography.labelMedium,
color = LocalContentColor.current.copy(
LocalContentColor.current.alpha * 0.7f
)
)
}
}
} else {
if (secondaryCal == null) {
val line1 = DateFormat.getInstanceForSkeleton(
primaryCal,
"EEEE",
ULocale.getDefault(),
).format(date)
val line2 =
DateFormat.getInstanceForSkeleton(
primaryCal,
"MMMM d",
ULocale.getDefault(),
).format(date)
Text(
text = "$line1\n$line2",
lineHeight = 1.2.em,
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Medium,
)
)
} else {
val line2 =
DateFormat.getInstanceForSkeleton(
secondaryCal,
"MMMM d",
ULocale.getDefault(),
).format(date)
Column {
Text(
text = DateFormat.getInstanceForSkeleton(
primaryCal,
"E MMMM d",
ULocale.getDefault(),
).format(date),
lineHeight = 1.2.em,
style = MaterialTheme.typography.titleLarge
)
Text(
text = DateFormat.getInstanceForSkeleton(
secondaryCal,
"MMMM d",
ULocale.getDefault(),
).format(date),
lineHeight = 1.2.em,
style = MaterialTheme.typography.labelLarge,
color = LocalContentColor.current.copy(
LocalContentColor.current.alpha * 0.7f
)
)
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.locals
import android.icu.util.Calendar
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
@@ -22,8 +23,10 @@ val LocalFavoritesEnabled = compositionLocalOf { true }
val LocalGridSettings = compositionLocalOf { GridSettings() }
val LocalTimeFormat = compositionLocalOf { TimeFormat.TwentyFourHour }
val LocalMeasurementSystem = compositionLocalOf { MeasurementSystem.Metric }
val LocalTimeFormat = staticCompositionLocalOf { TimeFormat.TwentyFourHour }
val LocalMeasurementSystem = staticCompositionLocalOf { MeasurementSystem.Metric }
val LocalCalendarSystems = staticCompositionLocalOf<List<Calendar?>> { listOf(null, null) }
val LocalCalendarSystemIds = staticCompositionLocalOf<List<String?>> { listOf(null, null) }
val LocalSnackbarHostState = compositionLocalOf { SnackbarHostState() }

View File

@@ -74,6 +74,8 @@ import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterRoute
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsRoute
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.debug.StringNormalizerTestRoute
import de.mm20.launcher2.ui.settings.debug.StringNormalizerTestScreen
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsRoute
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsRoute
@@ -96,6 +98,8 @@ import de.mm20.launcher2.ui.settings.integrations.IntegrationsSettingsRoute
import de.mm20.launcher2.ui.settings.integrations.IntegrationsSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseRoute
import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.locale.CalendarSettingsRoute
import de.mm20.launcher2.ui.settings.locale.CalendarSettingsScreen
import de.mm20.launcher2.ui.settings.locale.LocaleSettingsRoute
import de.mm20.launcher2.ui.settings.locale.LocaleSettingsScreen
import de.mm20.launcher2.ui.settings.locations.LocationsSettingsRoute
@@ -291,9 +295,15 @@ class SettingsActivity : BaseActivity() {
entry<DebugSettingsRoute> {
DebugSettingsScreen()
}
entry<StringNormalizerTestRoute> {
StringNormalizerTestScreen()
}
entry<LocaleSettingsRoute> {
LocaleSettingsScreen()
}
entry<CalendarSettingsRoute> {
CalendarSettingsScreen()
}
entry<BackupSettingsRoute> {
BackupSettingsScreen()
}

View File

@@ -6,7 +6,6 @@ import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -24,7 +23,6 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalBackStack
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterRoute
import de.mm20.launcher2.ui.settings.log.LogRoute
@@ -38,7 +36,7 @@ import java.text.SimpleDateFormat
import java.util.Date
@Serializable
data object DebugSettingsRoute: NavKey
data object DebugSettingsRoute : NavKey
@Composable
fun DebugSettingsScreen() {
@@ -133,6 +131,15 @@ fun DebugSettingsScreen() {
onClick = {
viewModel.reinstallIconPacks()
})
if (BuildConfig.DEBUG) {
Preference(
title = "String normalization test",
summary = "Test string transliteration and normalization",
onClick = {
backStack += StringNormalizerTestRoute
}
)
}
}
}
}

View File

@@ -0,0 +1,61 @@
package de.mm20.launcher2.ui.settings.debug
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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.unit.dp
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.search.StringNormalizer
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import kotlinx.serialization.Serializable
import org.koin.compose.koinInject
@Serializable
data object StringNormalizerTestRoute: NavKey
@Composable
fun StringNormalizerTestScreen() {
PreferenceScreen(
title = "String normalization test"
) {
item {
val normalizer: StringNormalizer = koinInject()
var string by remember { mutableStateOf("") }
val normalizedString by remember {
derivedStateOf {
normalizer.normalize(string)
}
}
PreferenceCategory {
Surface {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(16.dp),
value = string,
onValueChange = { string = it },
)
}
Surface {
Text(
modifier = Modifier.fillMaxWidth().padding(16.dp),
text = normalizedString,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
}

View File

@@ -0,0 +1,71 @@
package de.mm20.launcher2.ui.settings.locale
import android.icu.util.Calendar
import android.icu.util.ULocale
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.ListPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import kotlinx.serialization.Serializable
import java.text.Collator
@Serializable
data object CalendarSettingsRoute: NavKey
@Composable
fun CalendarSettingsScreen() {
val viewModel: CalendarSettingsScreenVM = viewModel()
val primaryCalendar by viewModel.primaryCalendar.collectAsState(null)
val secondaryCalendar by viewModel.secondaryCalendar.collectAsState(null)
val locale = ULocale.getDefault()
val calendars = remember(locale) {
val collator = Collator.getInstance().apply { strength = Collator.SECONDARY }
val cals = Calendar.getKeywordValuesForLocale("calendar", locale, false)
cals.map {
ULocale.getDefault().setKeywordValue("calendar", it).getDisplayKeywordValue("calendar") to it
}.sortedWith { el1, el2 ->
collator.compare(el1.first, el2.first)
}
}
PreferenceScreen(
title = stringResource(R.string.preference_calendar_system),
) {
item {
PreferenceCategory {
ListPreference(
title = stringResource(R.string.preference_calendar_system_primary),
items = listOf(
stringResource(R.string.preference_value_system_default) to null,
) + calendars,
value = primaryCalendar,
onValueChanged = {
viewModel.setPrimaryCalendar(it)
}
)
ListPreference(
title = stringResource(R.string.preference_calendar_system_secondary),
items = listOf(
stringResource(R.string.preference_value_disabled) to null
) + calendars,
value = secondaryCalendar,
onValueChanged = {
viewModel.setSecondaryCalendar(it)
}
)
}
}
}
}

View File

@@ -0,0 +1,22 @@
package de.mm20.launcher2.ui.settings.locale
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.ui.LocaleSettings
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class CalendarSettingsScreenVM: ViewModel(), KoinComponent {
private val localeSettings: LocaleSettings by inject()
val primaryCalendar = localeSettings.primaryCalendar
fun setPrimaryCalendar(calendar: String?) {
localeSettings.setPrimaryCalendar(calendar)
}
val secondaryCalendar = localeSettings.secondaryCalendar
fun setSecondaryCalendar(calendar: String?) {
localeSettings.setSecondaryCalendar(calendar)
}
}

View File

@@ -1,19 +1,16 @@
package de.mm20.launcher2.ui.settings.locale
import android.content.Intent
import android.icu.text.ListFormatter
import android.icu.text.Transliterator
import android.icu.util.ULocale
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.text.toUpperCase
import androidx.core.app.GrammaticalInflectionManagerCompat
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -28,8 +25,8 @@ import de.mm20.launcher2.ui.component.preferences.ListPreference
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalBackStack
import kotlinx.serialization.Serializable
import java.util.Locale
@Serializable
data object LocaleSettingsRoute : NavKey
@@ -40,9 +37,12 @@ fun LocaleSettingsScreen() {
val resources = LocalResources.current
val viewModel: LocaleSettingsScreenVM = viewModel()
val backstack = LocalBackStack.current
val timeFormat by viewModel.timeFormat.collectAsStateWithLifecycle(null)
val measurementSystem by viewModel.measurementSystem.collectAsStateWithLifecycle(null)
val transliterator by viewModel.transliterator.collectAsStateWithLifecycle(null)
val calendars by viewModel.calendars.collectAsStateWithLifecycle(emptyList())
// The language that has been selected by the user, or null to use the system language
val selectedLocale = remember {
@@ -57,13 +57,13 @@ fun LocaleSettingsScreen() {
val transliterators: List<Pair<String, String?>> = remember(locales) {
if (!isAtLeastApiLevel(29)) return@remember listOf()
if (locales?.isEmpty == true) return@remember listOf(resources.getString(R.string.preference_transliteration_disabled) to null)
if (locales?.isEmpty == true) return@remember listOf(resources.getString(R.string.preference_value_disabled) to null)
val scripts = mutableSetOf<String>()
val languages = mutableSetOf<String>()
val transliterators = mutableMapOf<String?, String>(
null to resources.getString(R.string.preference_transliteration_disabled),
null to resources.getString(R.string.preference_value_disabled),
"" to resources.getString(R.string.preference_transliteration_auto),
)
@@ -82,7 +82,11 @@ fun LocaleSettingsScreen() {
val ids = availableIds.filter { it.startsWith(filter) }
for (id in ids) {
transliterators[id] = "${ulocale.displayLanguage.replaceFirstChar { ulocale.displayLanguage.first().uppercase() }} ($id)"
transliterators[id] = "${
ulocale.displayLanguage.replaceFirstChar {
ulocale.displayLanguage.first().uppercase()
}
} ($id)"
}
languages.add(lng)
@@ -94,7 +98,11 @@ fun LocaleSettingsScreen() {
val ids = availableIds.filter { it.startsWith(filter) }
for (id in ids) {
transliterators[id] = "${ulocale.displayScript.replaceFirstChar { ulocale.displayScript.first().uppercase() }} ($id)"
transliterators[id] = "${
ulocale.displayScript.replaceFirstChar {
ulocale.displayScript.first().uppercase()
}
} ($id)"
}
scripts.add(ulocale.script)
}
@@ -197,6 +205,35 @@ fun LocaleSettingsScreen() {
stringResource(R.string.preference_measurement_system_us) to MeasurementSystem.UnitedStates,
)
)
Preference(
title = stringResource(R.string.preference_calendar_system),
icon = R.drawable.calendar_today_24px,
onClick = {
backstack += CalendarSettingsRoute
},
summary = remember(calendars) {
val primary = calendars.getOrNull(0)
val secondary = calendars.getOrNull(1)
val primaryName = if (primary == null) {
resources.getString(R.string.preference_value_system_default)
} else {
ULocale.getDefault().setKeywordValue("calendar", primary)
.getDisplayKeywordValue("calendar")
}
val secondaryName = if (secondary == null) {
null
} else {
ULocale.getDefault().setKeywordValue("calendar", secondary)
.getDisplayKeywordValue("calendar")
}
ListFormatter.getInstance().format(
listOfNotNull(primaryName, secondaryName)
)
}
)
}
}
}

View File

@@ -4,10 +4,12 @@ import androidx.lifecycle.ViewModel
import de.mm20.launcher2.preferences.MeasurementSystem
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.ui.LocaleSettings
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.combine
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class LocaleSettingsScreenVM: ViewModel(), KoinComponent {
class LocaleSettingsScreenVM : ViewModel(), KoinComponent {
private val localeSettings: LocaleSettings by inject()
val timeFormat = localeSettings.timeFormat
@@ -24,4 +26,8 @@ class LocaleSettingsScreenVM: ViewModel(), KoinComponent {
fun setTransliterator(transliterator: String?) {
localeSettings.setTransliterator(transliterator)
}
val calendars = combine(localeSettings.primaryCalendar, localeSettings.secondaryCalendar) {
it.toImmutableList()
}
}

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M200,880Q167,880 143.5,856.5Q120,833 120,800L120,240Q120,207 143.5,183.5Q167,160 200,160L240,160L240,120Q240,103 251.5,91.5Q263,80 280,80Q297,80 308.5,91.5Q320,103 320,120L320,160L640,160L640,120Q640,103 651.5,91.5Q663,80 680,80Q697,80 708.5,91.5Q720,103 720,120L720,160L760,160Q793,160 816.5,183.5Q840,207 840,240L840,800Q840,833 816.5,856.5Q793,880 760,880L200,880ZM200,800L760,800Q760,800 760,800Q760,800 760,800L760,400L200,400L200,800Q200,800 200,800Q200,800 200,800ZM200,320L760,320L760,240Q760,240 760,240Q760,240 760,240L200,240Q200,240 200,240Q200,240 200,240L200,320ZM200,320L200,240Q200,240 200,240Q200,240 200,240L200,240Q200,240 200,240Q200,240 200,240L200,320Z"/>
</vector>

View File

@@ -925,7 +925,7 @@
<string name="preference_tasks_integration_ready">تم الربط بنجاح. التطبيق جاهز للاستخدام.</string>
<string name="preference_transliteration">طريقة كتابة الأحرف</string>
<string name="preference_transliteration_auto">تلقائي</string>
<string name="preference_transliteration_disabled">معطل</string>
<string name="preference_value_disabled">معطل</string>
<string name="preference_value_system_default">إعدادات النظام</string>
<string name="preference_widgets_on_home_screen">أدوات الشاشة الرئيسية</string>
<string name="preference_widgets_on_home_screen_summary">عرض الأدوات في الشاشة الرئيسية بدلاً من صفحة منفصلة</string>

View File

@@ -889,7 +889,7 @@
<string name="preference_measurement_system_us">Spojené státy</string>
<string name="preference_transliteration">Preferovaná transkripce</string>
<string name="preference_transliteration_auto">Automatická</string>
<string name="preference_transliteration_disabled">Zakázaná</string>
<string name="preference_value_disabled">Zakázaná</string>
<string name="select_tag">Vybrat štítek</string>
<string name="widget_name_apps">Aplikace</string>
<string name="preference_category_enabled_plugins">Povoleno</string>

View File

@@ -878,7 +878,7 @@
<string name="preference_measurement_system_us">Vereinigte Staaten</string>
<string name="preference_transliteration">Bevorzugte Transliteration</string>
<string name="preference_transliteration_auto">Automatisch</string>
<string name="preference_transliteration_disabled">Deaktiviert</string>
<string name="preference_value_disabled">Deaktiviert</string>
<string name="preference_category_enabled_plugins">Aktiviert</string>
<string name="preference_category_installed_plugins">Installiert</string>
<string name="tag_icon_customicon">Icon</string>

View File

@@ -889,7 +889,7 @@
<string name="preference_measurement_system_us">Estados Unidos</string>
<string name="preference_transliteration">Transliteración preferida</string>
<string name="preference_transliteration_auto">Automática</string>
<string name="preference_transliteration_disabled">Deshabilitada</string>
<string name="preference_value_disabled">Deshabilitada</string>
<string name="preference_category_enabled_plugins">Activado</string>
<string name="preference_category_installed_plugins">Instalado</string>
<string name="widget_name_apps">Aplicaciones</string>

View File

@@ -849,6 +849,6 @@
<string name="preference_measurement_system_us">Ameriketako Estatu Batuak</string>
<string name="preference_transliteration">Hobetsitako transliterazioa</string>
<string name="preference_transliteration_auto">Automatikoa</string>
<string name="preference_transliteration_disabled">Ezgaitua</string>
<string name="preference_value_disabled">Ezgaitua</string>
<string name="select_tag">Etiketa hautatu</string>
</resources>

View File

@@ -878,7 +878,7 @@
<string name="preference_measurement_system_us">Egyesült Államok</string>
<string name="preference_transliteration">Előnyben részesített átírás</string>
<string name="preference_transliteration_auto">Automatikus</string>
<string name="preference_transliteration_disabled">Letiltva</string>
<string name="preference_value_disabled">Letiltva</string>
<string name="select_tag">Címke kiválasztása</string>
<string name="widget_name_apps">Alkalmazások</string>
<string name="preference_category_enabled_plugins">Engedélyezve</string>

View File

@@ -889,7 +889,7 @@
<string name="preference_measurement_system_uk">Regno Unito</string>
<string name="preference_transliteration">Traslitterazione preferita</string>
<string name="preference_transliteration_auto">Automatica</string>
<string name="preference_transliteration_disabled">Disabilitata</string>
<string name="preference_value_disabled">Disabilitata</string>
<string name="select_tag">Seleziona tag</string>
<string name="widget_name_apps">App</string>
<string name="preference_category_enabled_plugins">Abilitato</string>

View File

@@ -885,7 +885,7 @@
<string name="preference_measurement_system_metric">מטרית</string>
<string name="preference_measurement_system_uk">הממלכה המאוחדת</string>
<string name="preference_measurement_system_us">ארצות הברית</string>
<string name="preference_transliteration_disabled">מושבת</string>
<string name="preference_value_disabled">מושבת</string>
<string name="select_tag">בחירת תגית</string>
<string name="preference_transliteration">תעתיק מועדף</string>
<string name="preference_transliteration_auto">תעתיק אוטומטי</string>

View File

@@ -876,7 +876,7 @@
<string name="preference_measurement_system_metric">Metrisch</string>
<string name="preference_measurement_system_uk">Verenigd Koninkrijk</string>
<string name="preference_measurement_system_us">Verenigde Staten</string>
<string name="preference_transliteration_disabled">Uitgeschakeld</string>
<string name="preference_value_disabled">Uitgeschakeld</string>
<string name="preference_transliteration">Voorkeurstransliteratie</string>
<string name="preference_transliteration_auto">Automatisch</string>
<string name="select_tag">Label selecteren</string>

View File

@@ -878,6 +878,6 @@
<string name="preference_measurement_system_us">EUA</string>
<string name="preference_transliteration">Transliteração preferencial</string>
<string name="preference_transliteration_auto">Automática</string>
<string name="preference_transliteration_disabled">Inativa</string>
<string name="preference_value_disabled">Inativa</string>
<string name="select_tag">Selecione a etiqueta</string>
</resources>

View File

@@ -1082,7 +1082,7 @@
<string name="preference_measurement_system_us">Amerika Birleşik Devletleri</string>
<string name="preference_transliteration">Tercih edilen tercüme</string>
<string name="preference_transliteration_auto">Otomatik</string>
<string name="preference_transliteration_disabled">Devre dışı</string>
<string name="preference_value_disabled">Devre dışı</string>
<string name="select_tag">Etiket seçin</string>
<string name="action_quit">Çık</string>
<string name="preference_create_colors">Yeni renk şeması</string>

View File

@@ -872,7 +872,7 @@
<string name="preference_measurement_system_us">美制</string>
<string name="preference_transliteration">翻译语言偏好</string>
<string name="preference_transliteration_auto">自动</string>
<string name="preference_transliteration_disabled">禁用</string>
<string name="preference_value_disabled">禁用</string>
<string name="select_tag">选择标签</string>
<string name="action_quit">退出</string>
<string name="preference_clockwidget_smartspacer">由 Smartspacer 主管</string>

View File

@@ -876,7 +876,7 @@
<string name="preference_smartspacer_enable">啟用 Smartspacer 整合</string>
<string name="preference_launch_smartspacer_app">開啟 Smartspacer</string>
<string name="preference_transliteration">字母轉寫偏好</string>
<string name="preference_transliteration_disabled">停用</string>
<string name="preference_value_disabled">停用</string>
<string name="preference_feed_integration">Feed</string>
<string name="preference_category_feed_provider">Feed 提供者</string>
<string name="preference_feed_enable_summary">在主畫面向右滑動開啟 Feed</string>

View File

@@ -1085,6 +1085,7 @@
<string name="preference_screen_locale_summary">Language, units, time format</string>
<string name="preference_language">Language</string>
<string name="preference_value_system_default">System default</string>
<string name="preference_value_disabled">Disabled</string>
<string name="preference_form_of_address">Form of address</string>
<string name="preference_form_of_address_neutral">Neutral</string>
<string name="preference_form_of_address_fem">Feminine</string>
@@ -1095,7 +1096,9 @@
<string name="preference_measurement_system_us">United States</string>
<string name="preference_transliteration">Preferred transliteration</string>
<string name="preference_transliteration_auto">Automatic</string>
<string name="preference_transliteration_disabled">Disabled</string>
<string name="preference_calendar_system">Calendar system</string>
<string name="preference_calendar_system_primary">Primary calendar</string>
<string name="preference_calendar_system_secondary">Secondary calendar</string>
<string name="select_tag">Select tag</string>
<string name="create_tag_title">New tag</string>
<string name="edit_tag_title">Edit tag</string>

View File

@@ -202,6 +202,16 @@ data class LauncherSettingsData internal constructor(
*/
val localeTransliterator: String? = "",
/**
* The ICU id of the primary calendar. `null` to use the default.
*/
val localePrimaryCalendar: String? = null,
/**
* The ICU id of the secondary calendar. `null` to disable.
*/
val localeSecondaryCalendar: String? = null,
val feedProviderPackage: String? = null

View File

@@ -35,4 +35,22 @@ class LocaleSettings internal constructor(
it.copy(localeTransliterator = transliterator)
}
}
val primaryCalendar
get() = launcherDataStore.data.map { it.localePrimaryCalendar }
fun setPrimaryCalendar(primaryCalendar: String?) {
launcherDataStore.update {
it.copy(localePrimaryCalendar = primaryCalendar)
}
}
val secondaryCalendar
get() = launcherDataStore.data.map { it.localeSecondaryCalendar }
fun setSecondaryCalendar(secondaryCalendar: String?) {
launcherDataStore.update {
it.copy(localeSecondaryCalendar = secondaryCalendar)
}
}
}