mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 00:35:26 +02:00
Fix time zone search
This commit is contained in:
parent
ce8bc5c738
commit
bbacbf78f0
@ -21,6 +21,7 @@ package com.sadellie.unitto.data.common
|
||||
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 java.time.ZonedDateTime
|
||||
@ -44,6 +45,13 @@ fun TimeZone.regionName(
|
||||
return "$location, $region"
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun TimeZone.displayName(
|
||||
locale: ULocale,
|
||||
): String {
|
||||
return this.getDisplayName(locale) ?: id
|
||||
}
|
||||
|
||||
private val TimeZone.fallbackRegion: String
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
get() = id
|
||||
|
@ -21,9 +21,16 @@ package com.sadellie.unitto.data.model.timezone
|
||||
import android.icu.util.TimeZone
|
||||
|
||||
/**
|
||||
* Don't get 'region' from [timeZone]. Use [formattedLabel] (same but cached)
|
||||
* Use cached name and region properties!
|
||||
*
|
||||
* @property timeZone Same as [TimeZone]
|
||||
* @property name Cached localized [TimeZone.getDisplayName]
|
||||
* @property region Cached localized [TimeZone.getRegion]
|
||||
* @property rank Higher is better, closer to the search query
|
||||
*/
|
||||
data class SearchResultZone(
|
||||
val timeZone: TimeZone,
|
||||
val formattedLabel: String
|
||||
val name: String,
|
||||
val region: String,
|
||||
val rank: Int,
|
||||
)
|
||||
|
@ -24,6 +24,7 @@ import android.icu.util.TimeZone
|
||||
import android.icu.util.ULocale
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.sadellie.unitto.data.common.displayName
|
||||
import com.sadellie.unitto.data.common.lev
|
||||
import com.sadellie.unitto.data.common.regionName
|
||||
import com.sadellie.unitto.data.database.TimeZoneDao
|
||||
@ -40,7 +41,7 @@ import javax.inject.Singleton
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Singleton
|
||||
class TimeZonesRepository @Inject constructor(
|
||||
private val dao: TimeZoneDao
|
||||
private val dao: TimeZoneDao,
|
||||
) {
|
||||
// Not implemented because it will take me too much time to map 600+ TimeZones and codes
|
||||
// private val codeToTimeZoneId: HashMap<String, String> by lazy {
|
||||
@ -66,13 +67,13 @@ class TimeZonesRepository @Inject constructor(
|
||||
|
||||
suspend fun moveTimeZone(
|
||||
timeZone: FavoriteZone,
|
||||
targetPosition: Int
|
||||
targetPosition: Int,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
dao.moveMove(timeZone.timeZone.id, timeZone.position, targetPosition)
|
||||
}
|
||||
|
||||
suspend fun addToFavorites(
|
||||
timeZone: TimeZone
|
||||
timeZone: TimeZone,
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
dao.addToFavorites(timeZone.id)
|
||||
@ -80,37 +81,55 @@ class TimeZonesRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun removeFromFavorites(
|
||||
timeZone: FavoriteZone
|
||||
timeZone: FavoriteZone,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
dao.removeFromFavorites(timeZone.timeZone.id)
|
||||
}
|
||||
|
||||
suspend fun updateLabel(
|
||||
timeZone: FavoriteZone,
|
||||
label: String
|
||||
label: String,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
dao.updateLabel(timeZone.timeZone.id, label)
|
||||
}
|
||||
|
||||
suspend fun filterAllTimeZones(
|
||||
suspend fun filter(
|
||||
searchQuery: String,
|
||||
locale: ULocale,
|
||||
): List<SearchResultZone> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val favorites = dao.getFavorites().first().map { it.id }
|
||||
val timeZoneNames = TimeZoneNames.getInstance(locale)
|
||||
val localeDisplayNames = LocaleDisplayNames.getInstance(locale)
|
||||
|
||||
val favorites = dao.getFavorites().first().map { it.id }
|
||||
val timezones = TimeZone.getAvailableIDs()
|
||||
.filter { it !in favorites }
|
||||
.map { TimeZone.getTimeZone(it) }
|
||||
|
||||
if (searchQuery.isBlank()) {
|
||||
return@withContext timezones
|
||||
.map {
|
||||
val displayName = it.displayName(locale)
|
||||
val regionName = it.regionName(timeZoneNames, localeDisplayNames)
|
||||
|
||||
SearchResultZone(
|
||||
timeZone = it,
|
||||
name = displayName,
|
||||
region = regionName,
|
||||
rank = 0,
|
||||
)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
|
||||
val query = searchQuery.trim().lowercase()
|
||||
val threshold: Int = query.length / 2
|
||||
val timeZonesWithDist = mutableListOf<Pair<SearchResultZone, Int>>()
|
||||
val timeZonesWithDist = mutableSetOf<SearchResultZone>()
|
||||
|
||||
TimeZone.getAvailableIDs().forEach { timeZoneId ->
|
||||
if (timeZoneId in favorites) return@forEach
|
||||
|
||||
val timeZone = TimeZone.getTimeZone(timeZoneId)
|
||||
val displayName = timeZone.displayName
|
||||
val regionName = timeZone.regionName(timeZoneNames, localeDisplayNames)
|
||||
// Don't use map here so that only needed SearchResultZone objects will be created
|
||||
timezones.forEach {
|
||||
val displayName = it.displayName(locale)
|
||||
val regionName = it.regionName(timeZoneNames, localeDisplayNames)
|
||||
|
||||
// // CODE Match
|
||||
// if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) {
|
||||
@ -118,49 +137,50 @@ class TimeZonesRepository @Inject constructor(
|
||||
// return@forEach
|
||||
// }
|
||||
|
||||
// Display name match
|
||||
when {
|
||||
// not zero, so that lev can have that
|
||||
displayName.startsWith(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
displayName.contains(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
val displayNameLevDist = displayName
|
||||
.substring(0, minOf(query.length, displayName.length))
|
||||
.lev(query)
|
||||
if (displayNameLevDist < threshold) {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to displayNameLevDist)
|
||||
val nameMatch = matchProperty(displayName, query, threshold)
|
||||
if (nameMatch != null) {
|
||||
timeZonesWithDist.add(
|
||||
SearchResultZone(
|
||||
timeZone = it,
|
||||
name = displayName,
|
||||
region = regionName,
|
||||
rank = nameMatch
|
||||
)
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
// ID Match
|
||||
when {
|
||||
// not zero, so that lev can have that
|
||||
regionName.startsWith(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 1)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
regionName.contains(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to 2)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
val regionNameLevDist = regionName
|
||||
.substring(0, minOf(query.length, regionName.length))
|
||||
.lev(query)
|
||||
if (regionNameLevDist < threshold) {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, regionName) to regionNameLevDist)
|
||||
val regionMatch = matchProperty(regionName, query, threshold)
|
||||
if (regionMatch != null) {
|
||||
timeZonesWithDist.add(
|
||||
SearchResultZone(
|
||||
timeZone = it,
|
||||
name = displayName,
|
||||
region = regionName,
|
||||
rank = regionMatch
|
||||
)
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext timeZonesWithDist.sortedBy { it.second }.map { it.first }
|
||||
return@withContext timeZonesWithDist.sortedBy { it.rank }
|
||||
}
|
||||
|
||||
private fun matchProperty(
|
||||
prop: String,
|
||||
query: String,
|
||||
levThreshold: Int,
|
||||
): Int? {
|
||||
if (prop.startsWith(query, true)) return 1
|
||||
|
||||
if (prop.contains(query, true)) return 2
|
||||
|
||||
val levDist = prop
|
||||
.substring(0, minOf(query.length, prop.length))
|
||||
.lev(query)
|
||||
if (levDist < levThreshold) return levDist
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ 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
|
||||
@ -35,6 +34,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -48,6 +48,7 @@ import com.sadellie.unitto.core.ui.common.UnittoListItem
|
||||
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.displayName
|
||||
import com.sadellie.unitto.data.common.offset
|
||||
import com.sadellie.unitto.data.common.regionName
|
||||
import com.sadellie.unitto.data.model.timezone.SearchResultZone
|
||||
@ -82,7 +83,6 @@ fun AddTimeZoneScreen(
|
||||
userTime: ZonedDateTime,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val locale = ULocale.forLanguageTag(AppCompatDelegate.getApplicationLocales().toLanguageTags())
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
@ -109,8 +109,8 @@ fun AddTimeZoneScreen(
|
||||
addToFavorites(it.timeZone)
|
||||
navigateUp()
|
||||
},
|
||||
headlineContent = { Text(it.timeZone.getDisplayName(locale)) },
|
||||
supportingContent = { Text(it.formattedLabel) },
|
||||
headlineContent = { Text(it.name) },
|
||||
supportingContent = { Text(it.region) },
|
||||
trailingContent = {
|
||||
Text(
|
||||
text = it.timeZone.offset(userTime).formatLocal(),
|
||||
@ -130,6 +130,9 @@ fun AddTimeZoneScreen(
|
||||
@Composable
|
||||
fun PreviewAddTimeZoneScreen() {
|
||||
val locale = ULocale.getDefault()
|
||||
val timeZoneNames = remember(locale) { TimeZoneNames.getInstance(locale) }
|
||||
val localeDisplayNames = remember(locale) { LocaleDisplayNames.getInstance(locale) }
|
||||
|
||||
AddTimeZoneScreen(
|
||||
uiState = AddTimeZoneUIState.Ready(
|
||||
query = TextFieldValue(),
|
||||
@ -141,10 +144,12 @@ fun PreviewAddTimeZoneScreen() {
|
||||
val zone = TimeZone.getTimeZone(it)
|
||||
SearchResultZone(
|
||||
timeZone = zone,
|
||||
formattedLabel = zone.regionName(
|
||||
timeZoneNames = TimeZoneNames.getInstance(locale),
|
||||
localeDisplayNames = LocaleDisplayNames.getInstance(locale)
|
||||
)
|
||||
region = zone.regionName(
|
||||
timeZoneNames = timeZoneNames,
|
||||
localeDisplayNames = localeDisplayNames
|
||||
),
|
||||
name = zone.displayName(locale),
|
||||
rank = 0
|
||||
)
|
||||
}
|
||||
),
|
||||
|
@ -58,7 +58,7 @@ class AddTimeZoneViewModel @Inject constructor(
|
||||
.mapLatest { ui ->
|
||||
viewModelScope.launch {
|
||||
_result.update {
|
||||
timezonesRepository.filterAllTimeZones(
|
||||
timezonesRepository.filter(
|
||||
searchQuery = ui.query.text,
|
||||
locale = ULocale.forLanguageTag(
|
||||
AppCompatDelegate.getApplicationLocales().toLanguageTags()
|
||||
|
Loading…
x
Reference in New Issue
Block a user