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