diff --git a/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt b/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt index 4ef865fd..89b00b71 100644 --- a/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt +++ b/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt @@ -18,6 +18,8 @@ package com.sadellie.unitto.data.common +import android.icu.text.LocaleDisplayNames +import android.icu.text.TimeZoneNames import android.icu.util.TimeZone import android.os.Build import androidx.annotation.RequiresApi @@ -31,7 +33,18 @@ fun TimeZone.offset(currentTime: ZonedDateTime): ZonedDateTime { return currentTimeWithoutOffset.plusSeconds(this.rawOffset / 1000L) } -val TimeZone.region: String +@RequiresApi(Build.VERSION_CODES.N) +fun TimeZone.regionName( + timeZoneNames: TimeZoneNames, + localeDisplayNames: LocaleDisplayNames +): String { + val location = timeZoneNames.getExemplarLocationName(this.id) ?: return fallbackRegion + val region = localeDisplayNames.regionDisplayName(TimeZone.getRegion(id)) ?: return fallbackRegion + + return "$location, $region" +} + +private val TimeZone.fallbackRegion: String @RequiresApi(Build.VERSION_CODES.N) get() = id .replace("_", " ") diff --git a/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt b/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt index 2ed5b920..1ba9b7a5 100644 --- a/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt +++ b/data/timezone/src/main/java/com/sadellie/unitto/data/timezone/TimeZonesRepository.kt @@ -18,11 +18,14 @@ package com.sadellie.unitto.data.timezone +import android.icu.text.LocaleDisplayNames +import android.icu.text.TimeZoneNames import android.icu.util.TimeZone +import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi import com.sadellie.unitto.data.common.lev -import com.sadellie.unitto.data.common.region +import com.sadellie.unitto.data.common.regionName import com.sadellie.unitto.data.database.TimeZoneDao import com.sadellie.unitto.data.model.timezone.FavoriteZone import com.sadellie.unitto.data.model.timezone.SearchResultZone @@ -90,10 +93,13 @@ class TimeZonesRepository @Inject constructor( } suspend fun filterAllTimeZones( - searchQuery: String + searchQuery: String, + locale: ULocale, ): List = withContext(Dispatchers.IO) { val favorites = dao.getFavorites().first().map { it.id } + val timeZoneNames = TimeZoneNames.getInstance(locale) + val localeDisplayNames = LocaleDisplayNames.getInstance(locale) val query = searchQuery.trim().lowercase() val threshold: Int = query.length / 2 @@ -103,8 +109,8 @@ class TimeZonesRepository @Inject constructor( if (timeZoneId in favorites) return@forEach val timeZone = TimeZone.getTimeZone(timeZoneId) - val name = timeZone.displayName - val id = timeZone.region + val displayName = timeZone.displayName + val regionName = timeZone.regionName(timeZoneNames, localeDisplayNames) // // CODE Match // if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) { @@ -115,42 +121,42 @@ class TimeZonesRepository @Inject constructor( // Display name match when { // not zero, so that lev can have that - name.startsWith(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1) + displayName.startsWith(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1) return@forEach } - name.contains(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2) + displayName.contains(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2) return@forEach } } - val nameLevDist = name - .substring(0, minOf(query.length, name.length)) + val displayNameLevDist = displayName + .substring(0, minOf(query.length, displayName.length)) .lev(query) - if (nameLevDist < threshold) { - timeZonesWithDist.add(SearchResultZone(timeZone, id) to nameLevDist) + if (displayNameLevDist < threshold) { + timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to displayNameLevDist) return@forEach } // ID Match when { // not zero, so that lev can have that - id.startsWith(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1) + regionName.startsWith(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1) return@forEach } - id.contains(query) -> { - timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2) + regionName.contains(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2) return@forEach } } - val idLevDist = id - .substring(0, minOf(query.length, id.length)) + val regionNameLevDist = regionName + .substring(0, minOf(query.length, regionName.length)) .lev(query) - if (idLevDist < threshold) { - timeZonesWithDist.add(SearchResultZone(timeZone, id) to idLevDist) + if (regionNameLevDist < threshold) { + timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to regionNameLevDist) return@forEach } } diff --git a/feature/timezone/build.gradle.kts b/feature/timezone/build.gradle.kts index a98b3ba6..1c5a5006 100644 --- a/feature/timezone/build.gradle.kts +++ b/feature/timezone/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { debugImplementation(libs.androidx.compose.ui.test.manifest) implementation(libs.com.github.sadellie.themmo) implementation(libs.org.burnoutcrew.composereorderable.reorderable) + implementation(libs.androidx.appcompat.appcompat) implementation(project(":data:common")) implementation(project(":data:userprefs")) diff --git a/feature/timezone/src/androidTest/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZonesTest.kt b/feature/timezone/src/androidTest/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZonesTest.kt index 0aa014a1..1e5370d0 100644 --- a/feature/timezone/src/androidTest/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZonesTest.kt +++ b/feature/timezone/src/androidTest/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZonesTest.kt @@ -18,10 +18,14 @@ package com.sadellie.unitto.feature.timezone.components +import android.icu.text.LocaleDisplayNames +import android.icu.text.TimeZoneNames import android.icu.util.TimeZone +import android.icu.util.ULocale import androidx.activity.ComponentActivity import androidx.compose.foundation.background import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText @@ -38,6 +42,10 @@ class FavoriteTimeZonesTest { @Test fun convertTime(): Unit = with(composeTestRule) { setContent { + val locale = ULocale.getDefault() + val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) } + val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) } + FavoriteTimeZoneItem( modifier = Modifier .background(MaterialTheme.colorScheme.secondaryContainer), @@ -55,7 +63,9 @@ class FavoriteTimeZonesTest { onDelete = {}, onPrimaryClick = {}, onLabelClick = {}, - isDragging = false + isDragging = false, + timeZoneNames = timeZoneNames, + localeDisplayNames = localeDisplayNames ) } diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt index 46fd3007..5d74608d 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneScreen.kt @@ -18,9 +18,13 @@ package com.sadellie.unitto.feature.timezone +import android.icu.text.LocaleDisplayNames +import android.icu.text.TimeZoneNames import android.icu.util.TimeZone +import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable import androidx.compose.foundation.lazy.LazyColumn @@ -45,7 +49,7 @@ import com.sadellie.unitto.core.ui.common.UnittoSearchBar import com.sadellie.unitto.core.ui.datetime.formatLocal import com.sadellie.unitto.core.ui.theme.numberHeadlineSmall import com.sadellie.unitto.data.common.offset -import com.sadellie.unitto.data.common.region +import com.sadellie.unitto.data.common.regionName import com.sadellie.unitto.data.model.timezone.SearchResultZone import java.time.ZonedDateTime @@ -78,6 +82,7 @@ fun AddTimeZoneScreen( userTime: ZonedDateTime, ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -104,7 +109,7 @@ fun AddTimeZoneScreen( addToFavorites(it.timeZone) navigateUp() }, - headlineContent = { Text(it.timeZone.displayName) }, + headlineContent = { Text(it.timeZone.getDisplayName(locale)) }, supportingContent = { Text(it.formattedLabel) }, trailingContent = { Text( @@ -124,6 +129,7 @@ fun AddTimeZoneScreen( @Preview @Composable fun PreviewAddTimeZoneScreen() { + val locale = ULocale.getDefault() AddTimeZoneScreen( uiState = AddTimeZoneUIState.Ready( query = TextFieldValue(), @@ -135,7 +141,10 @@ fun PreviewAddTimeZoneScreen() { val zone = TimeZone.getTimeZone(it) SearchResultZone( timeZone = zone, - formattedLabel = zone.region + formattedLabel = zone.regionName( + timeZoneNames = TimeZoneNames.getInstance(locale), + localeDisplayNames = LocaleDisplayNames.getInstance(locale) + ) ) } ), diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt index ae445bc9..922d5d4a 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneViewModel.kt @@ -19,8 +19,10 @@ package com.sadellie.unitto.feature.timezone import android.icu.util.TimeZone +import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -55,7 +57,14 @@ class AddTimeZoneViewModel @Inject constructor( } .mapLatest { ui -> viewModelScope.launch { - _result.update { timezonesRepository.filterAllTimeZones(ui.query.text) } + _result.update { + timezonesRepository.filterAllTimeZones( + searchQuery = ui.query.text, + locale = ULocale.forLanguageTag( + AppCompatDelegate.getApplicationLocales().toLanguageTags() + ) + ) + } } ui } diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneScreen.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneScreen.kt index 9c49dd35..16450252 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneScreen.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneScreen.kt @@ -18,9 +18,13 @@ package com.sadellie.unitto.feature.timezone +import android.icu.text.LocaleDisplayNames +import android.icu.text.TimeZoneNames import android.icu.util.TimeZone +import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateInt import androidx.compose.animation.core.updateTransition @@ -137,6 +141,11 @@ private fun TimeZoneScreen( } val focusRequester = remember { FocusRequester() } + val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags()) + + val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) } + val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) } + LaunchedEffect(uiState.customUserTime) { while ((uiState.customUserTime == null) and isActive) { currentUserTime = uiState.userTimeZone.timeNow() @@ -247,7 +256,9 @@ private fun TimeZoneScreen( onPrimaryClick = { offsetTime -> setDialogState(TimeZoneDialogState.FavoriteTimePicker(item, offsetTime)) }, - isDragging = isDragging + isDragging = isDragging, + timeZoneNames = timeZoneNames, + localeDisplayNames = localeDisplayNames, ) } } diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZoneItem.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZoneItem.kt index 4a9fe390..e0a6e640 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZoneItem.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZoneItem.kt @@ -18,7 +18,10 @@ package com.sadellie.unitto.feature.timezone.components +import android.icu.text.LocaleDisplayNames +import android.icu.text.TimeZoneNames import android.icu.util.TimeZone +import android.icu.util.ULocale import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedContent @@ -65,6 +68,7 @@ import com.sadellie.unitto.core.ui.datetime.formatLocal import com.sadellie.unitto.core.ui.datetime.formatOffset import com.sadellie.unitto.core.ui.theme.numberHeadlineMedium import com.sadellie.unitto.data.common.offset +import com.sadellie.unitto.data.common.regionName import com.sadellie.unitto.data.model.timezone.FavoriteZone import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -81,6 +85,8 @@ internal fun FavoriteTimeZoneItem( onDelete: () -> Unit, onLabelClick: () -> Unit, onPrimaryClick: (ZonedDateTime) -> Unit, + timeZoneNames: TimeZoneNames, + localeDisplayNames: LocaleDisplayNames, ) { var deleteAnimationRunning by remember { mutableStateOf(false) } val animatedAlpha by animateFloatAsState( @@ -89,6 +95,10 @@ internal fun FavoriteTimeZoneItem( finishedListener = { if (it == 0f) onDelete() } ) + val regionName = remember(timeZoneNames, localeDisplayNames) { + item.timeZone.regionName(timeZoneNames, localeDisplayNames) + } + val offsetTime by remember(fromTime) { mutableStateOf(item.timeZone.offset(fromTime)) } val offsetTimeFormatted = offsetTime.formatOffset(fromTime) @@ -118,7 +128,7 @@ internal fun FavoriteTimeZoneItem( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Text( - text = item.timeZone.displayName, + text = regionName, style = MaterialTheme.typography.bodyLarge, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -298,6 +308,9 @@ private fun PreviewFavoriteTimeZones( @PreviewParameter(FavoriteTimeZoneItemParameterProvider::class) tz: FavoriteTimeZoneItemParameter, ) { var expanded by remember { mutableStateOf(tz.expanded) } + val locale = ULocale.getDefault() + val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) } + val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) } FavoriteTimeZoneItem( modifier = Modifier @@ -312,6 +325,8 @@ private fun PreviewFavoriteTimeZones( onDelete = {}, onPrimaryClick = {}, onLabelClick = {}, - isDragging = false + isDragging = false, + timeZoneNames = timeZoneNames, + localeDisplayNames = localeDisplayNames ) }