diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt index 681c8b78b..79b5fe006 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt @@ -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() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt index 712bd7f1b..3893412d1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt @@ -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)) } } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt index e4c135a42..384ff3bdd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt @@ -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())) } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt index ee6d40c15..d7d4613f3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt @@ -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 + ) + ) + } + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/locals/CompositionLocals.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/locals/CompositionLocals.kt index 0b6de5453..cd06b1c66 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/locals/CompositionLocals.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/locals/CompositionLocals.kt @@ -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> { listOf(null, null) } +val LocalCalendarSystemIds = staticCompositionLocalOf> { listOf(null, null) } val LocalSnackbarHostState = compositionLocalOf { SnackbarHostState() } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index 134801e81..91d7244d9 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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 { DebugSettingsScreen() } + entry { + StringNormalizerTestScreen() + } entry { LocaleSettingsScreen() } + entry { + CalendarSettingsScreen() + } entry { BackupSettingsScreen() } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt index 4f149c21b..df6fe1171 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt @@ -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 + } + ) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/StringNormalizerTestScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/StringNormalizerTestScreen.kt new file mode 100644 index 000000000..3fba7b69f --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/StringNormalizerTestScreen.kt @@ -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, + ) + } + } + } + + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/CalendarSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/CalendarSettingsScreen.kt new file mode 100644 index 000000000..cfaaa7d32 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/CalendarSettingsScreen.kt @@ -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) + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/CalendarSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/CalendarSettingsScreenVM.kt new file mode 100644 index 000000000..0709f754e --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/CalendarSettingsScreenVM.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt index 5030367cc..13e6f895a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreen.kt @@ -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> = 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() val languages = mutableSetOf() val transliterators = mutableMapOf( - 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) + ) + } + ) } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt index 8adbcbdc8..93d56f55e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/locale/LocaleSettingsScreenVM.kt @@ -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() + } } \ No newline at end of file diff --git a/core/base/src/main/res/drawable/calendar_today_24px.xml b/core/base/src/main/res/drawable/calendar_today_24px.xml new file mode 100644 index 000000000..c399684f5 --- /dev/null +++ b/core/base/src/main/res/drawable/calendar_today_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/i18n/src/main/res/values-ar/strings.xml b/core/i18n/src/main/res/values-ar/strings.xml index a6bcef0c2..b3d8302fc 100644 --- a/core/i18n/src/main/res/values-ar/strings.xml +++ b/core/i18n/src/main/res/values-ar/strings.xml @@ -925,7 +925,7 @@ تم الربط بنجاح. التطبيق جاهز للاستخدام. طريقة كتابة الأحرف تلقائي - معطل + معطل إعدادات النظام أدوات الشاشة الرئيسية عرض الأدوات في الشاشة الرئيسية بدلاً من صفحة منفصلة diff --git a/core/i18n/src/main/res/values-cs/strings.xml b/core/i18n/src/main/res/values-cs/strings.xml index 8ed9a8672..23a9fb021 100644 --- a/core/i18n/src/main/res/values-cs/strings.xml +++ b/core/i18n/src/main/res/values-cs/strings.xml @@ -889,7 +889,7 @@ Spojené státy Preferovaná transkripce Automatická - Zakázaná + Zakázaná Vybrat štítek Aplikace Povoleno diff --git a/core/i18n/src/main/res/values-de/strings.xml b/core/i18n/src/main/res/values-de/strings.xml index 8054b85bf..ecc6625e6 100644 --- a/core/i18n/src/main/res/values-de/strings.xml +++ b/core/i18n/src/main/res/values-de/strings.xml @@ -878,7 +878,7 @@ Vereinigte Staaten Bevorzugte Transliteration Automatisch - Deaktiviert + Deaktiviert Aktiviert Installiert Icon diff --git a/core/i18n/src/main/res/values-es/strings.xml b/core/i18n/src/main/res/values-es/strings.xml index 68a5cbfaf..17d50df9f 100644 --- a/core/i18n/src/main/res/values-es/strings.xml +++ b/core/i18n/src/main/res/values-es/strings.xml @@ -889,7 +889,7 @@ Estados Unidos Transliteración preferida Automática - Deshabilitada + Deshabilitada Activado Instalado Aplicaciones diff --git a/core/i18n/src/main/res/values-eu/strings.xml b/core/i18n/src/main/res/values-eu/strings.xml index d0b1257a2..30e8326cb 100644 --- a/core/i18n/src/main/res/values-eu/strings.xml +++ b/core/i18n/src/main/res/values-eu/strings.xml @@ -849,6 +849,6 @@ Ameriketako Estatu Batuak Hobetsitako transliterazioa Automatikoa - Ezgaitua + Ezgaitua Etiketa hautatu diff --git a/core/i18n/src/main/res/values-hu/strings.xml b/core/i18n/src/main/res/values-hu/strings.xml index e29389231..8de021b6d 100644 --- a/core/i18n/src/main/res/values-hu/strings.xml +++ b/core/i18n/src/main/res/values-hu/strings.xml @@ -878,7 +878,7 @@ Egyesült Államok Előnyben részesített átírás Automatikus - Letiltva + Letiltva Címke kiválasztása Alkalmazások Engedélyezve diff --git a/core/i18n/src/main/res/values-it/strings.xml b/core/i18n/src/main/res/values-it/strings.xml index 7297bcb4b..4724df5ac 100644 --- a/core/i18n/src/main/res/values-it/strings.xml +++ b/core/i18n/src/main/res/values-it/strings.xml @@ -889,7 +889,7 @@ Regno Unito Traslitterazione preferita Automatica - Disabilitata + Disabilitata Seleziona tag App Abilitato diff --git a/core/i18n/src/main/res/values-iw/strings.xml b/core/i18n/src/main/res/values-iw/strings.xml index 4a2865a2f..683ca406e 100644 --- a/core/i18n/src/main/res/values-iw/strings.xml +++ b/core/i18n/src/main/res/values-iw/strings.xml @@ -885,7 +885,7 @@ מטרית הממלכה המאוחדת ארצות הברית - מושבת + מושבת בחירת תגית תעתיק מועדף תעתיק אוטומטי diff --git a/core/i18n/src/main/res/values-nl/strings.xml b/core/i18n/src/main/res/values-nl/strings.xml index 963c4c5a6..6c56555ce 100644 --- a/core/i18n/src/main/res/values-nl/strings.xml +++ b/core/i18n/src/main/res/values-nl/strings.xml @@ -876,7 +876,7 @@ Metrisch Verenigd Koninkrijk Verenigde Staten - Uitgeschakeld + Uitgeschakeld Voorkeurstransliteratie Automatisch Label selecteren diff --git a/core/i18n/src/main/res/values-pt/strings.xml b/core/i18n/src/main/res/values-pt/strings.xml index a4d98c213..f7275e0dc 100644 --- a/core/i18n/src/main/res/values-pt/strings.xml +++ b/core/i18n/src/main/res/values-pt/strings.xml @@ -878,6 +878,6 @@ EUA Transliteração preferencial Automática - Inativa + Inativa Selecione a etiqueta diff --git a/core/i18n/src/main/res/values-tr/strings.xml b/core/i18n/src/main/res/values-tr/strings.xml index d4de9bd0b..11e8d2264 100644 --- a/core/i18n/src/main/res/values-tr/strings.xml +++ b/core/i18n/src/main/res/values-tr/strings.xml @@ -1082,7 +1082,7 @@ Amerika Birleşik Devletleri Tercih edilen tercüme Otomatik - Devre dışı + Devre dışı Etiket seçin Çık Yeni renk şeması diff --git a/core/i18n/src/main/res/values-zh-rCN/strings.xml b/core/i18n/src/main/res/values-zh-rCN/strings.xml index e4e706587..40d1054d3 100644 --- a/core/i18n/src/main/res/values-zh-rCN/strings.xml +++ b/core/i18n/src/main/res/values-zh-rCN/strings.xml @@ -872,7 +872,7 @@ 美制 翻译语言偏好 自动 - 禁用 + 禁用 选择标签 退出 由 Smartspacer 主管 diff --git a/core/i18n/src/main/res/values-zh-rTW/strings.xml b/core/i18n/src/main/res/values-zh-rTW/strings.xml index c88379a83..8891ff2ab 100644 --- a/core/i18n/src/main/res/values-zh-rTW/strings.xml +++ b/core/i18n/src/main/res/values-zh-rTW/strings.xml @@ -876,7 +876,7 @@ 啟用 Smartspacer 整合 開啟 Smartspacer 字母轉寫偏好 - 停用 + 停用 Feed Feed 提供者 在主畫面向右滑動開啟 Feed diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 8c5c75c42..d14c7cd91 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -1085,6 +1085,7 @@ Language, units, time format Language System default + Disabled Form of address Neutral Feminine @@ -1095,7 +1096,9 @@ United States Preferred transliteration Automatic - Disabled + Calendar system + Primary calendar + Secondary calendar Select tag New tag Edit tag diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index 8c9d294e1..141229ced 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -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 diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt index 3ef6f58ce..9a7e85ebd 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/LocaleSettings.kt @@ -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) + } + } } \ No newline at end of file