diff --git a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt index b7cda27b..e6e49b7c 100644 --- a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt +++ b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt @@ -70,17 +70,6 @@ internal fun UnittoApp(prefs: AppPreferences?) { val shortcutsScope = rememberCoroutineScope() - val tabs by remember { - mutableStateOf( - listOf( - DrawerItems.Calculator, - DrawerItems.Converter, - DrawerItems.DateDifference, - DrawerItems.TimeZones - ) - ) - } - val navBackStackEntry by navController.currentBackStackEntryAsState() val gesturesEnabled: Boolean by remember(navBackStackEntry?.destination) { derivedStateOf { @@ -115,7 +104,7 @@ internal fun UnittoApp(prefs: AppPreferences?) { drawer = { UnittoDrawerSheet( modifier = Modifier, - tabs = tabs, + tabs = DrawerItems.ALL, currentDestination = navBackStackEntry?.destination?.route ) { destination -> drawerScope.launch { drawerState.close() } diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt index e8fa2424..a7b581f6 100644 --- a/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt +++ b/core/base/src/main/java/com/sadellie/unitto/core/base/TopLevelDestinations.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.core.base +import android.os.Build import androidx.annotation.DrawableRes import androidx.annotation.StringRes @@ -102,12 +103,20 @@ sealed class TopLevelDestinations( // Shown in settings val TOP_LEVEL_DESTINATIONS by lazy { - listOf( - TopLevelDestinations.Calculator, - TopLevelDestinations.Converter, - TopLevelDestinations.DateCalculator, - TopLevelDestinations.TimeZone, - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + listOf( + TopLevelDestinations.Calculator, + TopLevelDestinations.Converter, + TopLevelDestinations.DateCalculator, + TopLevelDestinations.TimeZone, + ) + } else { + listOf( + TopLevelDestinations.Calculator, + TopLevelDestinations.Converter, + TopLevelDestinations.DateCalculator, + ) + } } // Only routes, not graphs! diff --git a/core/base/src/main/res/values/strings.xml b/core/base/src/main/res/values/strings.xml index dc5e755e..1144d3c6 100644 --- a/core/base/src/main/res/values/strings.xml +++ b/core/base/src/main/res/values/strings.xml @@ -1,11 +1,12 @@ + + + Add Unitto All expressions from history will be deleted forever. This action can\'t be undone! Can\'t divide by 0 No history - - Calculator Cancel Checked filter @@ -14,8 +15,6 @@ Clear Click to try again Add or remove unit from favorites - - Convert from Make sure there are no typos, try different filters or check for disabled unit groups. Convert to @@ -43,16 +42,15 @@ Years + Delete Disabled Open or close drop down menu Enabled - - - Epoch converter Error Hello! - + + Label Loading… Deutsch English @@ -65,8 +63,6 @@ Dutch Русский Türkçe - - Navigate up No results found OK Open menu - Open settings Open settings Search button Search… @@ -102,15 +97,11 @@ Used in this dialog window. Should be short --> Dark Disable unit group Display - - App look and feel Dynamic colors Use colors from your wallpaper Enable unit group Exponential notation - - Replace part of the number with E Format time Example: Show 130 minutes as 2h 10m @@ -134,8 +125,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d Precision Converted values may have a precision higher than the preferred one. %1$s (Max) - - Number of decimal places Privacy Policy Rate this app @@ -143,14 +132,10 @@ Maybe this can be labeled better? Let me know. It should be something that can d Selected color Selected style Separator - - Group separator symbol Alphabetical Scale (Asc.) Scale (Desc.) - - Usage Space Starting screen @@ -162,9 +147,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d System font Use system font for texts in app Terms and Conditions - - - Themes Third party licenses Settings Translate this app @@ -184,57 +166,35 @@ Maybe this can be labeled better? Let me know. It should be something that can d ac Minute \' - - Second \" Apostilb asb Dalton u - - Attofarad aF Attojoule aJ - - Attoliter aL - - Attometer - - Attometer/square second am/s^2 am Attonewton aN - - Attopascal aPa - - Attosecond as - - Attowatt aW Bar bar - - Binary base2 - - Bit - - Bit/second b/s b @@ -254,14 +214,10 @@ Maybe this can be labeled better? Let me know. It should be something that can d cd/ft^2 Candela/square inch cd/in^2 - - Candela/square meter cd/m^2 Carat ct - - Celsius °C Cent @@ -278,8 +234,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d cm Centipascal cPa - - Unit converter First Cosmic Velocity v1 @@ -313,8 +267,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d Cubic Millimeter/second mm3/s mm^3 - - 1inch Network NCH Cardano @@ -765,16 +717,10 @@ Maybe this can be labeled better? Let me know. It should be something that can d Earth g Earth\'s orbital speed ve - - Electron cross section ecs - - Electron mass me - - Electron volt eV Horse power @@ -881,8 +827,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d Flux Force Fuel - - Length Luminance Mass @@ -1018,8 +962,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d Light year ly Liter - - Liter/hour L/h Liter/minute @@ -1043,8 +985,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d Mars M Mars surface gravity Mars g - - Maxwell Mx Mebibit @@ -1144,8 +1084,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d Millimeter Millimeter of mercury mm Hg - - Millimeter/hour mm/h Millimeter/minute @@ -1186,13 +1124,9 @@ Maybe this can be labeled better? Let me know. It should be something that can d Neptune M Neptune surface gravity Neptune g - - Newton Newton centimeter N*cm - - Newton meter N*m Newton millimeter @@ -1280,8 +1214,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d p Quecto q - - Quetta Q Ronna @@ -1422,8 +1354,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d Yard/second yd/s yd - - y Yesterday \ No newline at end of file diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoSearchBar.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoSearchBar.kt index 2b30f920..c8679b6c 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoSearchBar.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoSearchBar.kt @@ -68,7 +68,7 @@ fun UnittoSearchBar( title: String, searchActions: @Composable (RowScope.() -> Unit) = {}, noSearchActions: @Composable (RowScope.() -> Unit) = {}, - placeholder: String, + placeholder: String = stringResource(R.string.search_text_field_placeholder), scrollBehavior: TopAppBarScrollBehavior? = null, colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors() ) { diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/UnittoDateTimeFormatter.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/UnittoDateTimeFormatter.kt index d95e770a..3f727bee 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/UnittoDateTimeFormatter.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/UnittoDateTimeFormatter.kt @@ -36,6 +36,26 @@ data object UnittoDateTimeFormatter { */ val time12Formatter1: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("hh:mm") } + /** + * 23 + */ + val time24OnlyHoursFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("HH") } + + /** + * 23 + */ + val time12OnlyHoursFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("hh") } + + /** + * 59 + */ + val timeOnlyMinutesFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("mm") } + + /** + * 59 + */ + val timeOnlySecondsFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("ss") } + /** * AM */ diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/ZonedDateTimeUtils.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/ZonedDateTimeUtils.kt index 1938c5d8..86d0dba3 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/ZonedDateTimeUtils.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/datetime/ZonedDateTimeUtils.kt @@ -27,12 +27,42 @@ import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue +/** + * Formats date time into something like: + * + * 23:59 or 11:59 AM + * + * Depends on system preferences + * + * @return Formatted string + */ @Composable fun ZonedDateTime.formatLocal(): String { return if (DateFormat.is24HourFormat(LocalContext.current)) format(UnittoDateTimeFormatter.time24Formatter) else format(UnittoDateTimeFormatter.time12FormatterFull) } +@Composable +fun ZonedDateTime.formatOnlyHours(): String { + return if (DateFormat.is24HourFormat(LocalContext.current)) format(UnittoDateTimeFormatter.time24OnlyHoursFormatter) + else format(UnittoDateTimeFormatter.time12OnlyHoursFormatter) +} + +@Composable +fun ZonedDateTime.formatOnlyMinutes(): String { + return format(UnittoDateTimeFormatter.timeOnlyMinutesFormatter) +} + +@Composable +fun ZonedDateTime.formatOnlySeconds(): String { + return format(UnittoDateTimeFormatter.timeOnlySecondsFormatter) +} + +@Composable +fun ZonedDateTime.formatOnlyAmPm(): String { + return format(UnittoDateTimeFormatter.time12Formatter2) +} + /** * Format offset string. Examples: * @@ -74,7 +104,7 @@ fun ZonedDateTime.formatOffset( resultBuffer += "${hour}${stringResource(R.string.unit_hour_short)}" } - // TODO Very ugly + // TODO Very ugly. Replace with formatTime option from unit converter if (minute != 0L) { if (hour != 0L) resultBuffer += " " resultBuffer += "${minute}${stringResource(R.string.unit_minute_short)}" diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt index b22bad75..d876efec 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/model/DrawerItems.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.core.ui.model +import android.os.Build import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Calculate import androidx.compose.material.icons.filled.Event @@ -58,4 +59,23 @@ sealed class DrawerItems( selectedIcon = Icons.Filled.Schedule, defaultIcon = Icons.Outlined.Schedule ) + + companion object { + val ALL by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + listOf( + Calculator, + Converter, + DateDifference, + TimeZones + ) + } else { + listOf( + Calculator, + Converter, + DateDifference, + ) + } + } + } } diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt index a021bd49..94a2edd0 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt @@ -56,6 +56,46 @@ val Typography.numbersDisplayMedium by lazy { ) } +val Typography.numberBodyLarge: TextStyle by lazy { + TextStyle( + fontFamily = latoFamily, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 1.5.em, + letterSpacing = 0.5.sp, + ) +} + +val Typography.numberDisplayLarge: TextStyle by lazy { + TextStyle( + fontFamily = latoFamily, + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 1.25.em, + letterSpacing = (-0.25).sp, + ) +} + +val Typography.numberHeadlineSmall: TextStyle by lazy { + TextStyle( + fontFamily = latoFamily, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 1.25.em, + letterSpacing = 0.sp, + ) +} + +val Typography.numberHeadlineMedium: TextStyle by lazy { + TextStyle( + fontFamily = latoFamily, + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 1.25.em, + letterSpacing = 0.sp, + ) +} + val TypographyUnitto by lazy { Typography( displayLarge = TextStyle( 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 new file mode 100644 index 00000000..4ef865fd --- /dev/null +++ b/data/common/src/main/java/com/sadellie/unitto/data/common/TimeZoneUtils.kt @@ -0,0 +1,40 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.data.common + +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi +import java.time.ZonedDateTime + +@RequiresApi(Build.VERSION_CODES.N) +fun TimeZone.offset(currentTime: ZonedDateTime): ZonedDateTime { + val offsetSeconds = currentTime.offset.totalSeconds.toLong() + val currentTimeWithoutOffset = currentTime.minusSeconds(offsetSeconds) + + return currentTimeWithoutOffset.plusSeconds(this.rawOffset / 1000L) +} + +val TimeZone.region: String + @RequiresApi(Build.VERSION_CODES.N) + get() = id + .replace("_", " ") + .split("/") + .reversed() + .joinToString() diff --git a/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt index 3de4f9d4..1984def3 100644 --- a/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt +++ b/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneDao.kt @@ -19,23 +19,59 @@ package com.sadellie.unitto.data.database import androidx.room.Dao -import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import kotlinx.coroutines.flow.Flow @Dao interface TimeZoneDao { - @Query("SELECT * FROM time_zones ORDER BY position ASC") - fun getAll(): Flow> + + @Query("SELECT * FROM time_zones WHERE position > 0 ORDER BY position ASC") + fun getFavorites(): Flow> + + @Query("SELECT MAX(position) FROM time_zones") + fun getMaxPosition(): Int @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(vararg timeZoneEntity: TimeZoneEntity) - @Query("UPDATE time_zones SET position = ( SELECT SUM(position) FROM time_zones WHERE id IN (:fromId, :toId) ) - position WHERE id IN (:fromId, :toId)") - suspend fun swap(fromId: String, toId: String) + @Transaction + suspend fun addToFavorites(id: String) { + insert( + TimeZoneEntity( + id = id, + position = getMaxPosition() + 1, + ) + ) + } - @Delete - suspend fun remove(timeZoneEntity: TimeZoneEntity) + @Query("UPDATE time_zones SET position = -1 WHERE id = :id") + suspend fun removeFromFavorites(id: String) + + @Query("UPDATE time_zones SET label = :label WHERE id = :id") + suspend fun updateLabel(id: String, label: String) + + @Query("UPDATE time_zones SET position = :newPosition WHERE position = :oldPosition AND id = :id") + suspend fun updateDragged(id: String, oldPosition: Int, newPosition: Int) + + @Query("UPDATE time_zones SET position = (position - 1) WHERE position > :currentPosition and position <= :targetPosition") + suspend fun moveDown(currentPosition: Int, targetPosition: Int) + + @Query("UPDATE time_zones SET position = (position + 1) WHERE position >= :targetPosition AND position < :currentPosition") + suspend fun moveUp(currentPosition: Int, targetPosition: Int) + + @Transaction + suspend fun moveMove(id: String, currentPosition: Int, targetPosition: Int) { + // Very good explanation + // https://www.c-sharpcorner.com/article/updating-display-order-in-database-with-drag-drop5/ + updateDragged(id, currentPosition, 0) + if (targetPosition > currentPosition) { + moveDown(currentPosition, targetPosition) + } else { + moveUp(currentPosition, targetPosition) + } + updateDragged(id, 0, targetPosition) + } } diff --git a/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneEntity.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneEntity.kt index 91383b85..d5bd296a 100644 --- a/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneEntity.kt +++ b/data/database/src/main/java/com/sadellie/unitto/data/database/TimeZoneEntity.kt @@ -25,6 +25,6 @@ import androidx.room.PrimaryKey @Entity(tableName = "time_zones") class TimeZoneEntity( @PrimaryKey val id: String, - @ColumnInfo(name = "position") val position: Int, + @ColumnInfo(name = "position") val position: Int, @ColumnInfo(name = "label") val label: String = "", ) diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/FavoriteZone.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/FavoriteZone.kt new file mode 100644 index 00000000..7c7e7f1b --- /dev/null +++ b/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/FavoriteZone.kt @@ -0,0 +1,30 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.data.model.timezone + +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi + +@RequiresApi(Build.VERSION_CODES.N) +data class FavoriteZone( + val timeZone: TimeZone, + val position: Int, + val label: String +) diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/SearchResultZone.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/SearchResultZone.kt new file mode 100644 index 00000000..3c4f3a32 --- /dev/null +++ b/data/model/src/main/java/com/sadellie/unitto/data/model/timezone/SearchResultZone.kt @@ -0,0 +1,29 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.data.model.timezone + +import android.icu.util.TimeZone + +/** + * Don't get 'region' from [timeZone]. Use [formattedLabel] (same but cached) + */ +data class SearchResultZone( + val timeZone: TimeZone, + val formattedLabel: String +) 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 91a453e5..2ed5b920 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,97 +18,143 @@ package com.sadellie.unitto.data.timezone +import android.icu.util.TimeZone +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.database.TimeZoneDao -import com.sadellie.unitto.data.database.TimeZoneEntity -import com.sadellie.unitto.data.model.UnittoTimeZone +import com.sadellie.unitto.data.model.timezone.FavoriteZone +import com.sadellie.unitto.data.model.timezone.SearchResultZone import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton +@RequiresApi(Build.VERSION_CODES.N) @Singleton class TimeZonesRepository @Inject constructor( private val dao: TimeZoneDao ) { - private val allTimeZones: HashMap = hashMapOf( - "zulu_time_zone" to UnittoTimeZone(id = "zulu_time_zone", nameRes = "Zulu Time Zone", offsetSeconds = 0) - ) +// Not implemented because it will take me too much time to map 600+ TimeZones and codes +// private val codeToTimeZoneId: HashMap by lazy { +// hashMapOf() +// } - val favoriteTimeZones: Flow> = dao - .getAll() + val favoriteTimeZones: Flow> = dao + .getFavorites() .map { list -> - val favorites = mutableListOf() + val favorites = mutableListOf() list.forEach { entity -> - val foundTimeZone = allTimeZones[entity.id] ?: return@forEach - val mapped = foundTimeZone.copy( - position = entity.position + favorites.add( + FavoriteZone( + timeZone = TimeZone.getTimeZone(entity.id), + position = entity.position, + label = entity.label + ) ) - favorites.add(mapped) } favorites } - suspend fun swapTimeZones(from: String, to: String) = withContext(Dispatchers.IO) { - dao.swap(from, to) - - return@withContext + suspend fun moveTimeZone( + timeZone: FavoriteZone, + targetPosition: Int + ) = withContext(Dispatchers.IO) { + dao.moveMove(timeZone.timeZone.id, timeZone.position, targetPosition) } - suspend fun delete(timeZone: UnittoTimeZone) = withContext(Dispatchers.IO) { - // Only PrimaryKey is needed - dao.remove(TimeZoneEntity(id = timeZone.id, position = 0)) - } - - suspend fun filterAllTimeZones(searchQuery: String): List = + suspend fun addToFavorites( + timeZone: TimeZone + ) { withContext(Dispatchers.IO) { + dao.addToFavorites(timeZone.id) + } + } + + suspend fun removeFromFavorites( + timeZone: FavoriteZone + ) = withContext(Dispatchers.IO) { + dao.removeFromFavorites(timeZone.timeZone.id) + } + + suspend fun updateLabel( + timeZone: FavoriteZone, + label: String + ) = withContext(Dispatchers.IO) { + dao.updateLabel(timeZone.timeZone.id, label) + } + + suspend fun filterAllTimeZones( + searchQuery: String + ): List = + withContext(Dispatchers.IO) { + val favorites = dao.getFavorites().first().map { it.id } + val query = searchQuery.trim().lowercase() val threshold: Int = query.length / 2 - val timeZonesWithDist = mutableListOf>() + val timeZonesWithDist = mutableListOf>() - allTimeZones.values.forEach { timeZone -> - val timeZoneName = timeZone.nameRes + TimeZone.getAvailableIDs().forEach { timeZoneId -> + if (timeZoneId in favorites) return@forEach - if (timeZone.code.lowercase() == query) { - timeZonesWithDist.add(timeZone to 1) + val timeZone = TimeZone.getTimeZone(timeZoneId) + val name = timeZone.displayName + val id = timeZone.region + +// // CODE Match +// if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) { +// timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1) +// return@forEach +// } + + // Display name match + when { + // not zero, so that lev can have that + name.startsWith(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1) + return@forEach + } + + name.contains(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2) + return@forEach + } + } + val nameLevDist = name + .substring(0, minOf(query.length, name.length)) + .lev(query) + if (nameLevDist < threshold) { + timeZonesWithDist.add(SearchResultZone(timeZone, id) to nameLevDist) return@forEach } + // ID Match when { // not zero, so that lev can have that - timeZoneName.startsWith(query) -> { - timeZonesWithDist.add(timeZone to 1) + id.startsWith(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1) return@forEach } - timeZoneName.contains(query) -> { - timeZonesWithDist.add(timeZone to 2) + id.contains(query) -> { + timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2) return@forEach } } - - val levDist = timeZoneName - .substring(0, minOf(query.length, timeZoneName.length)) + val idLevDist = id + .substring(0, minOf(query.length, id.length)) .lev(query) - - if (levDist < threshold) { - timeZonesWithDist.add(timeZone to levDist) + if (idLevDist < threshold) { + timeZonesWithDist.add(SearchResultZone(timeZone, id) to idLevDist) + return@forEach } } return@withContext timeZonesWithDist.sortedBy { it.second }.map { it.first } } - - suspend fun addToFavorites(timeZone: UnittoTimeZone) { -// UNCOMMENT FOR RELEASE - dao.insert( - TimeZoneEntity( - id = timeZone.id, - position = System.currentTimeMillis().toInt() - ) - ) - } } diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ChipsRow.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ChipsRow.kt index 0ab23c81..8739150d 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ChipsRow.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ChipsRow.kt @@ -106,7 +106,7 @@ internal fun ChipsRow( AssistChip( onClick = navigateToSettingsAction, imageVector = Icons.Default.Settings, - contentDescription = stringResource(R.string.open_settings_description) + contentDescription = stringResource(R.string.open_settings_label) ) } } @@ -159,7 +159,7 @@ fun ChipsFlexRow( AssistChip( onClick = navigateToSettingsAction, imageVector = Icons.Default.Settings, - contentDescription = stringResource(R.string.open_settings_description) + contentDescription = stringResource(R.string.open_settings_label) ) } diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/SearchPlaceholder.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/SearchPlaceholder.kt index 777d1df9..cfa1f11a 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/SearchPlaceholder.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/SearchPlaceholder.kt @@ -66,7 +66,7 @@ internal fun SearchPlaceholder(navigateToSettingsAction: () -> Unit) { ) // Open settings button ElevatedButton(onClick = navigateToSettingsAction) { - Text(text = stringResource(R.string.open_settings_description)) + Text(text = stringResource(R.string.open_settings_label)) } } } diff --git a/feature/timezone/build.gradle.kts b/feature/timezone/build.gradle.kts index c8c3b176..a98b3ba6 100644 --- a/feature/timezone/build.gradle.kts +++ b/feature/timezone/build.gradle.kts @@ -30,6 +30,9 @@ android { dependencies { testImplementation(libs.junit.junit) + testImplementation(libs.org.robolectric.robolectric) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.test.manifest) implementation(libs.com.github.sadellie.themmo) implementation(libs.org.burnoutcrew.composereorderable.reorderable) 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 new file mode 100644 index 00000000..0aa014a1 --- /dev/null +++ b/feature/timezone/src/androidTest/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZonesTest.kt @@ -0,0 +1,65 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.timezone.components + +import android.icu.util.TimeZone +import androidx.activity.ComponentActivity +import androidx.compose.foundation.background +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import com.sadellie.unitto.data.model.timezone.FavoriteZone +import org.junit.Rule +import org.junit.Test +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class FavoriteTimeZonesTest { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun convertTime(): Unit = with(composeTestRule) { + setContent { + FavoriteTimeZoneItem( + modifier = Modifier + .background(MaterialTheme.colorScheme.secondaryContainer), + item = FavoriteZone( + timeZone = TimeZone.getTimeZone("Africa/Addis_Ababa"), + position = -1, + label = "label text" + ), + fromTime = ZonedDateTime.parse( + "2023-05-01T14:00+03:00[Africa/Addis_Ababa]", + DateTimeFormatter.ISO_ZONED_DATE_TIME + ), + expanded = true, + onClick = {}, + onDelete = {}, + onPrimaryClick = {}, + onLabelClick = {}, + isDragging = false + ) + } + + onNodeWithText("11:00").assertExists() + } + +} \ No newline at end of file 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 b03dbc7d..46fd3007 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,138 +18,130 @@ package com.sadellie.unitto.feature.timezone -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.tween +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sadellie.unitto.core.base.R +import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen +import com.sadellie.unitto.core.ui.common.UnittoListItem import com.sadellie.unitto.core.ui.common.UnittoSearchBar -import com.sadellie.unitto.data.model.UnittoTimeZone -import com.sadellie.unitto.feature.timezone.components.SelectableTimeZone -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive +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.model.timezone.SearchResultZone import java.time.ZonedDateTime +@RequiresApi(Build.VERSION_CODES.N) @Composable internal fun AddTimeZoneRoute( viewModel: AddTimeZoneViewModel = hiltViewModel(), navigateUp: () -> Unit, - userTime: ZonedDateTime? = null + userTime: ZonedDateTime, ) { - val uiState = viewModel.addTimeZoneUIState.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { - if (userTime == null) { - while (isActive) { - viewModel.setTime(ZonedDateTime.now()) - delay(1000) - } - } else { - viewModel.setTime(userTime) - } + when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) { + AddTimeZoneUIState.Loading -> UnittoEmptyScreen() + is AddTimeZoneUIState.Ready -> AddTimeZoneScreen( + uiState = uiState, + navigateUp = navigateUp, + onQueryChange = viewModel::onQueryChange, + addToFavorites = viewModel::addToFavorites, + userTime = userTime + ) } - - AddTimeZoneScreen( - uiState = uiState.value, - navigateUp = navigateUp, - onQueryChange = viewModel::onQueryChange, - addToFavorites = viewModel::addToFavorites, - ) } +@RequiresApi(Build.VERSION_CODES.N) @Composable fun AddTimeZoneScreen( - uiState: AddTimeZoneUIState, + uiState: AddTimeZoneUIState.Ready, navigateUp: () -> Unit, onQueryChange: (TextFieldValue) -> Unit, - addToFavorites: (UnittoTimeZone) -> Unit, + addToFavorites: (TimeZone) -> Unit, + userTime: ZonedDateTime, ) { - val listState = rememberLazyListState() - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( - rememberTopAppBarState() - ) - val elevatedColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) - val needToTint by remember { - derivedStateOf { scrollBehavior.state.overlappedFraction > 0.01f } - } - - val searchBarBackground = animateColorAsState( - targetValue = if (needToTint) elevatedColor else MaterialTheme.colorScheme.surface, - animationSpec = tween(durationMillis = 500, easing = LinearOutSlowInEasing), - label = "Search bar background" - ) + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { UnittoSearchBar( - modifier = Modifier, query = uiState.query, onQueryChange = onQueryChange, navigateUp = navigateUp, title = stringResource(R.string.time_zone_add_title), - placeholder = stringResource(R.string.search_text_field_placeholder), - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = searchBarBackground.value - ) + scrollBehavior = scrollBehavior ) }, ) { paddingValues -> - LazyColumn( - modifier = Modifier.padding(paddingValues), - state = listState - ) { - items(uiState.list) { - SelectableTimeZone( - timeZone = it, - modifier = Modifier - .clickable { addToFavorites(it); navigateUp() } - .fillMaxWidth(), - currentTime = uiState.userTime - ) + Crossfade(targetState = uiState.list.isEmpty()) { empty -> + if (empty) { + UnittoEmptyScreen() + } else { + LazyColumn(contentPadding = paddingValues) { + items(uiState.list, { it.timeZone.id }) { + UnittoListItem( + modifier = Modifier + .animateItemPlacement() + .clickable { + addToFavorites(it.timeZone) + navigateUp() + }, + headlineContent = { Text(it.timeZone.displayName) }, + supportingContent = { Text(it.formattedLabel) }, + trailingContent = { + Text( + text = it.timeZone.offset(userTime).formatLocal(), + style = MaterialTheme.typography.numberHeadlineSmall + ) + } + ) + } + } } } } } +@RequiresApi(Build.VERSION_CODES.N) @Preview @Composable fun PreviewAddTimeZoneScreen() { AddTimeZoneScreen( - navigateUp = {}, - uiState = AddTimeZoneUIState( - list = List(50) { - UnittoTimeZone( - id = "timezone $it", - nameRes = "Time zone $it", + uiState = AddTimeZoneUIState.Ready( + query = TextFieldValue(), + list = listOf( + "UTC", + "Africa/Addis_Ababa", + "ACT" + ).map { + val zone = TimeZone.getTimeZone(it) + SearchResultZone( + timeZone = zone, + formattedLabel = zone.region ) } ), + navigateUp = {}, onQueryChange = {}, addToFavorites = {}, + userTime = ZonedDateTime.now() ) } diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneUIState.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneUIState.kt index 977650e6..dc4dd75a 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneUIState.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/AddTimeZoneUIState.kt @@ -19,11 +19,13 @@ package com.sadellie.unitto.feature.timezone import androidx.compose.ui.text.input.TextFieldValue -import com.sadellie.unitto.data.model.UnittoTimeZone -import java.time.ZonedDateTime +import com.sadellie.unitto.data.model.timezone.SearchResultZone -data class AddTimeZoneUIState( - val query: TextFieldValue = TextFieldValue(), - val list: List = emptyList(), - val userTime: ZonedDateTime? = null, -) +sealed class AddTimeZoneUIState { + data object Loading: AddTimeZoneUIState() + + data class Ready( + val query: TextFieldValue, + val list: List, + ): AddTimeZoneUIState() +} 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 8f266b51..ae445bc9 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 @@ -18,66 +18,52 @@ package com.sadellie.unitto.feature.timezone +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sadellie.unitto.data.model.UnittoTimeZone +import com.sadellie.unitto.data.common.stateIn +import com.sadellie.unitto.data.model.timezone.SearchResultZone import com.sadellie.unitto.data.timezone.TimeZonesRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.time.ZonedDateTime import javax.inject.Inject +@RequiresApi(Build.VERSION_CODES.N) @HiltViewModel class AddTimeZoneViewModel @Inject constructor( private val timezonesRepository: TimeZonesRepository, ) : ViewModel() { - - private val _userTime = MutableStateFlow(ZonedDateTime.now()) private val _query = MutableStateFlow(TextFieldValue()) + private val _result = MutableStateFlow(emptyList()) - private val _filteredTimeZones = MutableStateFlow(emptyList()) - val addTimeZoneUIState = combine( + val uiState = combine( _query, - _filteredTimeZones, - _userTime, - ) { query, filteredTimeZone, userTime -> - return@combine AddTimeZoneUIState( + _result, + timezonesRepository.favoriteTimeZones, + ) { query, result, _ -> + return@combine AddTimeZoneUIState.Ready( query = query, - list = filteredTimeZone, - userTime = userTime, + list = result, ) - }.stateIn( - viewModelScope, SharingStarted.WhileSubscribed(5000), AddTimeZoneUIState() - ) - - fun onQueryChange(query: TextFieldValue) { - _query.update { query } - filterTimeZones(query.text) } - - private fun filterTimeZones(query: String = "") = viewModelScope.launch { - _filteredTimeZones.update { - timezonesRepository.filterAllTimeZones(query) + .mapLatest { ui -> + viewModelScope.launch { + _result.update { timezonesRepository.filterAllTimeZones(ui.query.text) } + } + ui } - } + .stateIn(viewModelScope, AddTimeZoneUIState.Loading) - fun addToFavorites(timeZone: UnittoTimeZone) = viewModelScope.launch(Dispatchers.IO) { + fun onQueryChange(textFieldValue: TextFieldValue) = _query.update { textFieldValue } + + fun addToFavorites(timeZone: TimeZone) = viewModelScope.launch { timezonesRepository.addToFavorites(timeZone) } - - fun setTime(time: ZonedDateTime) = viewModelScope.launch(Dispatchers.Default) { - _userTime.update { time } - } - - init { - // TODO Maybe only when actually needed? - filterTimeZones() - } } diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/UnittoTimeZone.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneExt.kt similarity index 58% rename from data/model/src/main/java/com/sadellie/unitto/data/model/UnittoTimeZone.kt rename to feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneExt.kt index 7ba757e7..92a7fee1 100644 --- a/data/model/src/main/java/com/sadellie/unitto/data/model/UnittoTimeZone.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneExt.kt @@ -16,22 +16,18 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.data.model +package com.sadellie.unitto.feature.timezone +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi +import java.time.ZoneId import java.time.ZonedDateTime -data class UnittoTimeZone( - val id: String, - // For beta only, will change to StringRes later - val nameRes: String, - val position: Int = 0, - val offsetSeconds: Long = 0, - val code: String = "CODE", -) { - fun offsetFrom(currentTime: ZonedDateTime): ZonedDateTime { - val offsetSeconds = currentTime.offset.totalSeconds.toLong() - val currentTimeWithoutOffset = currentTime.minusSeconds(offsetSeconds) +val TimeZone.offsetSeconds + @RequiresApi(Build.VERSION_CODES.N) + get() = this.rawOffset / 1000L - return currentTimeWithoutOffset.plusSeconds(this.offsetSeconds) - } -} + +@RequiresApi(Build.VERSION_CODES.N) +fun TimeZone.timeNow(): ZonedDateTime = ZonedDateTime.now(ZoneId.of(this.id, ZoneId.SHORT_IDS)) 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 f0a1b462..6934cd94 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,61 +18,52 @@ package com.sadellie.unitto.feature.timezone -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.SizeTransform -import androidx.compose.animation.animateColor +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.animateInt import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.animateTo -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.outlined.History +import androidx.compose.material3.AlertDialog import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFloatingActionButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -81,102 +72,112 @@ import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.ui.common.MenuButton import com.sadellie.unitto.core.ui.common.SettingsButton import com.sadellie.unitto.core.ui.common.TimePickerDialog +import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar -import com.sadellie.unitto.core.ui.common.squashable -import com.sadellie.unitto.core.ui.datetime.UnittoDateTimeFormatter -import com.sadellie.unitto.core.ui.datetime.formatLocal -import com.sadellie.unitto.core.ui.theme.TypographyUnitto -import com.sadellie.unitto.core.ui.theme.DarkThemeColors -import com.sadellie.unitto.core.ui.theme.LightThemeColors -import com.sadellie.unitto.data.model.UnittoTimeZone -import com.sadellie.unitto.feature.timezone.components.TimeZoneListItem -import io.github.sadellie.themmo.Themmo -import io.github.sadellie.themmo.rememberThemmoController +import com.sadellie.unitto.data.model.timezone.FavoriteZone +import com.sadellie.unitto.feature.timezone.components.FavoriteTimeZoneItem +import com.sadellie.unitto.feature.timezone.components.UserTimeZone +import kotlinx.coroutines.android.awaitFrame import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import org.burnoutcrew.reorderable.ReorderableItem import org.burnoutcrew.reorderable.detectReorderAfterLongPress import org.burnoutcrew.reorderable.rememberReorderableLazyListState import org.burnoutcrew.reorderable.reorderable import java.time.ZonedDateTime +@RequiresApi(Build.VERSION_CODES.N) @Composable internal fun TimeZoneRoute( viewModel: TimeZoneViewModel = hiltViewModel(), - navigateToMenu: () -> Unit, + openMenu: () -> Unit, navigateToSettings: () -> Unit, - navigateToAddTimeZone: (ZonedDateTime?) -> Unit, + navigateToAddTimeZone: (ZonedDateTime) -> Unit, ) { - val uiState = viewModel.timeZoneUIState.collectAsStateWithLifecycle() - - TimeZoneScreen( - uiState = uiState.value, - navigateToMenu = navigateToMenu, - navigateToSettings = navigateToSettings, - navigateToAddTimeZone = { - navigateToAddTimeZone( - if (uiState.value.updateTime) null - else uiState.value.userTime + when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) { + TimeZoneUIState.Loading -> UnittoEmptyScreen() + is TimeZoneUIState.Ready -> { + TimeZoneScreen( + uiState = uiState, + openMenu = openMenu, + navigateToSettings = navigateToSettings, + navigateToAddTimeZone = navigateToAddTimeZone, + setCurrentTime = viewModel::setCurrentTime, + setSelectedTime = viewModel::setSelectedTime, + onDragEnd = viewModel::onDragEnd, + delete = viewModel::delete, + updateLabel = viewModel::updateLabel, + selectTimeZone = viewModel::selectTimeZone, + setDialogState = viewModel::setDialogState ) - }, - onDragEnd = viewModel::onDragEnd, - onDelete = viewModel::onDelete, - setSelectedTime = viewModel::setCustomTime, - setCurrentTime = viewModel::setCurrentTime, - resetTime = viewModel::resetTime, - ) + } + } } +@RequiresApi(Build.VERSION_CODES.N) @Composable private fun TimeZoneScreen( - uiState: TimeZoneUIState, - navigateToMenu: () -> Unit, + uiState: TimeZoneUIState.Ready, + openMenu: () -> Unit, navigateToSettings: () -> Unit, - navigateToAddTimeZone: () -> Unit, - onDragEnd: (String, String) -> Unit, - onDelete: (UnittoTimeZone) -> Unit, - setSelectedTime: (ZonedDateTime) -> Unit, + navigateToAddTimeZone: (ZonedDateTime) -> Unit, setCurrentTime: () -> Unit, - resetTime: () -> Unit, + setSelectedTime: (ZonedDateTime) -> Unit, + onDragEnd: (id: FavoriteZone, target: Int) -> Unit, + delete: (FavoriteZone) -> Unit, + updateLabel: (FavoriteZone, String) -> Unit, + selectTimeZone: (FavoriteZone?) -> Unit, + setDialogState: (TimeZoneDialogState) -> Unit, ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + var currentUserTime by remember(uiState.customUserTime) { + mutableStateOf( + uiState.customUserTime ?: uiState.userTimeZone.timeNow() + ) + } + val focusRequester = remember { FocusRequester() } - LaunchedEffect(uiState.updateTime) { - while (uiState.updateTime and isActive) { - setCurrentTime() + LaunchedEffect(uiState.customUserTime) { + while ((uiState.customUserTime == null) and isActive) { + currentUserTime = uiState.userTimeZone.timeNow() delay(1000) } } - val copiedList = rememberUpdatedState(newValue = uiState.list) as MutableState + val copiedList = rememberUpdatedState(newValue = uiState.favorites) as MutableState val state = rememberReorderableLazyListState( - onMove = onMove@{ from, to -> + onMove = { from, to -> // -1 because we use fake item. It fixes animation for the first item in list copiedList.value = copiedList.value .toMutableList() .apply { add(to.index - 1, removeAt(from.index - 1)) } - onDragEnd(from.key as String, to.key as String) }, canDragOver = { draggedOver, _ -> // Don't allow dragging over fake item draggedOver.index > 0 + }, + onDragEnd = onDragEnd@{ from, to -> + if (from == to) return@onDragEnd + // There is some logic going on. I have no idea what I did here but it works + val tz = copiedList.value.getOrNull(to - 1) ?: return@onDragEnd + val targetInOldTz = uiState.favorites.getOrNull(to - 1) ?: return@onDragEnd + + onDragEnd(tz, targetInOldTz.position) } ) - // TODO Unswipe on dragging - var swiped by remember> { mutableStateOf(null) } - var showTimeSelector by rememberSaveable { mutableStateOf(false) } - val maxDrag = -with(LocalDensity.current) { 80.dp.toPx() } UnittoScreenWithTopBar( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), title = { Text(stringResource(R.string.time_zone_title)) }, - navigationIcon = { MenuButton(navigateToMenu) }, + navigationIcon = { MenuButton(openMenu) }, actions = { SettingsButton(navigateToSettings) }, floatingActionButton = { - LargeFloatingActionButton(navigateToAddTimeZone) { + LargeFloatingActionButton( + onClick = { + navigateToAddTimeZone(currentUserTime) + } + ) { Icon( imageVector = Icons.Filled.Add, contentDescription = null, @@ -186,194 +187,183 @@ private fun TimeZoneScreen( }, floatingActionButtonPosition = FabPosition.Center, scrollBehavior = scrollBehavior, - - ) { paddingValues -> - + ) { padding -> LazyColumn( state = state.listState, modifier = Modifier - .padding(paddingValues) - .fillMaxHeight() - .reorderable(state) - .detectReorderAfterLongPress(state), - verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(bottom = 124.dp) + .fillMaxSize() + .padding(padding) + .reorderable(state), + contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 124.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - // This is a fake item. First item in list can not animated, so we do this magic fuckery - item { + item("user time") { UserTimeZone( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - userTime = uiState.userTime, - onClick = { showTimeSelector = true }, - onResetClick = resetTime, - showReset = !uiState.updateTime, + .padding(8.dp), + userTime = currentUserTime, + onClick = { setDialogState(TimeZoneDialogState.UserTimePicker(currentUserTime)) }, + onResetClick = setCurrentTime, + showReset = uiState.customUserTime != null ) } - items(copiedList.value, { it.id }) { item -> - ReorderableItem(state, key = item.id) { isDragging -> + + items(copiedList.value, { it.timeZone.id }) { item -> + ReorderableItem( + reorderableState = state, + key = item.timeZone.id, + ) { isDragging -> + val isSelected = uiState.selectedTimeZone == item + val transition = updateTransition(isDragging, label = "draggedTransition") - val background by transition.animateColor(label = "background") { - if (it) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer - } + val itemPadding by transition.animateDp(label = "itemPadding") { - if (it) 32.dp else 16.dp + if (it) 8.dp else 0.dp } - val scope = rememberCoroutineScope() - val draggableState = rememberDraggableTimeZone(maxDrag) { swiped = item } - - LaunchedEffect(swiped) { - if (swiped != item) scope.launch { - draggableState.animateTo(false) - } + val elevation by transition.animateDp(label = "elevation") { + if (it) 8.dp else 2.dp } - TimeZoneListItem( + val cornerRadius by transition.animateInt(label = "cornerRadius") { + if (it) 25 else 15 + } + + FavoriteTimeZoneItem( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = itemPadding), - timeZone = item, - currentTime = uiState.userTime, - onDelete = onDelete, - color = background, - onSwipe = {}, - draggableState = draggableState, + .padding(itemPadding) + .clip(RoundedCornerShape(cornerRadius)) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)) + .detectReorderAfterLongPress(state), + item = item, + fromTime = currentUserTime, + expanded = isSelected, + onClick = { + selectTimeZone(if (isSelected) null else item) + }, + onDelete = { delete(item) }, + onLabelClick = { setDialogState(TimeZoneDialogState.LabelEditPicker(item)) }, + onPrimaryClick = { offsetTime -> + setDialogState(TimeZoneDialogState.FavoriteTimePicker(item, offsetTime)) + }, + isDragging = isDragging ) } } } } - if (showTimeSelector) { - TimePickerDialog( - hour = uiState.userTime.hour, - minute = uiState.userTime.minute, - onConfirm = { hour, minute -> - setSelectedTime( - uiState.userTime - .withHour(hour) - .withMinute(minute) + when (uiState.dialogState) { + is TimeZoneDialogState.UserTimePicker -> { + TimePickerDialog( + hour = currentUserTime.hour, + minute = currentUserTime.minute, + onConfirm = { hour, minute -> + setSelectedTime( + currentUserTime + .withHour(hour) + .withMinute(minute) + ) + setDialogState(TimeZoneDialogState.Nothing) + }, + onDismiss = { setDialogState(TimeZoneDialogState.Nothing) } + ) + } + + is TimeZoneDialogState.FavoriteTimePicker -> { + TimePickerDialog( + hour = uiState.dialogState.time.hour, + minute = uiState.dialogState.time.minute, + onConfirm = { hour, minute -> + setSelectedTime( + uiState.dialogState.time + .withHour(hour) + .withMinute(minute) + .minusSeconds(uiState.dialogState.timeZone.timeZone.offsetSeconds) + .plusSeconds(uiState.userTimeZone.offsetSeconds) + + ) + setDialogState(TimeZoneDialogState.Nothing) + }, + onDismiss = { setDialogState(TimeZoneDialogState.Nothing) } + ) + } + + is TimeZoneDialogState.LabelEditPicker -> { + var tfv by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue( + text = uiState.dialogState.timeZone.label, + selection = TextRange(uiState.dialogState.timeZone.label.length) + ) ) - showTimeSelector = false - }, - onDismiss = { showTimeSelector = false } - ) + } + AlertDialog( + title = { Text(text = stringResource(R.string.label_label)) }, + text = { + OutlinedTextField( + value = tfv, + onValueChange = { tfv = it }, + modifier = Modifier.focusRequester(focusRequester) + ) + LaunchedEffect(Unit) { + awaitFrame() + focusRequester.requestFocus() + } + }, + confirmButton = { + TextButton( + onClick = { + setDialogState(TimeZoneDialogState.Nothing) + updateLabel(uiState.dialogState.timeZone, tfv.text) + }, + content = { Text(text = stringResource(R.string.ok_label)) } + ) + }, + dismissButton = { + TextButton( + onClick = { setDialogState(TimeZoneDialogState.Nothing) }, + content = { Text(text = stringResource(R.string.cancel_label)) } + ) + }, + onDismissRequest = { setDialogState(TimeZoneDialogState.Nothing) }, + ) + } + + TimeZoneDialogState.Nothing -> Unit } } -@Composable -private fun UserTimeZone( - modifier: Modifier, - userTime: ZonedDateTime, - onClick: () -> Unit, - onResetClick: () -> Unit, - showReset: Boolean, -) { - - Row( - modifier = modifier - .squashable( - onClick = onClick, - onLongClick = onResetClick, - cornerRadiusRange = 8.dp..32.dp, - interactionSource = remember { MutableInteractionSource() } - ) - .background(MaterialTheme.colorScheme.tertiaryContainer) - .padding(horizontal = 16.dp, vertical = 12.dp), - ) { - Column(Modifier.weight(1f)) { - Text( - text = userTime.format(UnittoDateTimeFormatter.zoneFormatPattern), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onTertiaryContainer - ) - AnimatedContent( - targetState = userTime.formatLocal(), - label = "user time change", - transitionSpec = { - slideInVertically { height -> height } + fadeIn() togetherWith - slideOutVertically { height -> -height } + fadeOut() using - SizeTransform() - } - ) { time -> - Text( - text = time, - style = MaterialTheme.typography.displayLarge, - color = MaterialTheme.colorScheme.onTertiaryContainer - ) - } - Text( - text = userTime.format(UnittoDateTimeFormatter.dayMonthYear), - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onTertiaryContainer - ) - } - AnimatedVisibility( - visible = showReset, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut(), - ) { - IconButton(onResetClick) { - Icon( - imageVector = Icons.Outlined.History, - contentDescription = null, - tint = MaterialTheme.colorScheme.onTertiaryContainer, - ) - } - } - } -} - -@Composable -private fun rememberDraggableTimeZone( - maxDrag: Float, - onSwipe: () -> Unit, -) = remember { - AnchoredDraggableState( - initialValue = false, - anchors = DraggableAnchors { - false at 0f - true at maxDrag - }, - positionalThreshold = { it * 0.5f }, - velocityThreshold = { maxDrag }, - animationSpec = tween(), - confirmValueChange = { - onSwipe() - true - } - ) -} - +@RequiresApi(Build.VERSION_CODES.N) @Preview @Composable fun PreviewTimeZoneScreen() { - Themmo( - themmoController = rememberThemmoController( - lightColorScheme = LightThemeColors, - darkColorScheme = DarkThemeColors, - ), - typography = TypographyUnitto, - ) { - TimeZoneScreen( - uiState = TimeZoneUIState( - list = List(50) { - UnittoTimeZone( - id = "timezone $it", - nameRes = "Time zone $it", + TimeZoneScreen( + uiState = TimeZoneUIState.Ready( + favorites = TimeZone + .getAvailableIDs() + .mapIndexed { index, tz -> + FavoriteZone( + timeZone = TimeZone.getTimeZone(tz), + position = index, + label = if (tz == "ACT") "label text" else "" ) - } - ), - navigateToMenu = {}, - navigateToSettings = {}, - navigateToAddTimeZone = {}, - onDragEnd = { _, _ -> }, - onDelete = {}, - setSelectedTime = {}, - setCurrentTime = {}, - resetTime = {}, - ) - } + }, + customUserTime = null, + userTimeZone = TimeZone.getTimeZone("Africa/Addis_Ababa"), + selectedTimeZone = null, + dialogState = TimeZoneDialogState.Nothing + ), + openMenu = {}, + navigateToSettings = {}, + navigateToAddTimeZone = {}, + setCurrentTime = {}, + setSelectedTime = {}, + onDragEnd = { _, _ -> }, + delete = {}, + updateLabel = { _, _ -> }, + selectTimeZone = {}, + setDialogState = {} + ) } diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneUIState.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneUIState.kt index 5b0ef725..50bc1b05 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneUIState.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneUIState.kt @@ -18,11 +18,35 @@ package com.sadellie.unitto.feature.timezone -import com.sadellie.unitto.data.model.UnittoTimeZone +import android.icu.util.TimeZone +import com.sadellie.unitto.data.model.timezone.FavoriteZone import java.time.ZonedDateTime -data class TimeZoneUIState( - val list: List = emptyList(), - val userTime: ZonedDateTime = ZonedDateTime.now(), - val updateTime: Boolean = true, -) +internal sealed class TimeZoneUIState { + data object Loading : TimeZoneUIState() + + data class Ready( + val favorites: List, + val customUserTime: ZonedDateTime?, + val userTimeZone: TimeZone, + val selectedTimeZone: FavoriteZone?, + val dialogState: TimeZoneDialogState, + ) : TimeZoneUIState() +} + +internal sealed class TimeZoneDialogState { + data object Nothing : TimeZoneDialogState() + + data class UserTimePicker( + val time: ZonedDateTime, + ) : TimeZoneDialogState() + + data class FavoriteTimePicker( + val timeZone: FavoriteZone, + val time: ZonedDateTime, + ) : TimeZoneDialogState() + + data class LabelEditPicker( + val timeZone: FavoriteZone, + ) : TimeZoneDialogState() +} diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneViewModel.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneViewModel.kt index efb9f3c8..accf81e9 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneViewModel.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/TimeZoneViewModel.kt @@ -18,61 +18,72 @@ package com.sadellie.unitto.feature.timezone +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sadellie.unitto.data.model.UnittoTimeZone +import com.sadellie.unitto.data.common.stateIn +import com.sadellie.unitto.data.model.timezone.FavoriteZone import com.sadellie.unitto.data.timezone.TimeZonesRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.time.ZonedDateTime import javax.inject.Inject +@RequiresApi(Build.VERSION_CODES.N) @HiltViewModel -class TimeZoneViewModel @Inject constructor( +internal class TimeZoneViewModel @Inject constructor( private val timezonesRepository: TimeZonesRepository ): ViewModel() { + private val _userTimeZone = MutableStateFlow(TimeZone.getDefault()) + private val _customUserTime = MutableStateFlow(null) + private val _selectedTimeZone = MutableStateFlow(null) + private val _dialogState = MutableStateFlow(TimeZoneDialogState.Nothing) - private val _userTime = MutableStateFlow(ZonedDateTime.now()) - private val _updateTime = MutableStateFlow(true) - - val timeZoneUIState = combine( - _userTime, - _updateTime, - timezonesRepository.favoriteTimeZones - ) { userTime, updateTime, favorites -> - return@combine TimeZoneUIState( - list = favorites, - userTime = userTime, - updateTime = updateTime + val uiState = combine( + _customUserTime, + _userTimeZone, + _selectedTimeZone, + _dialogState, + timezonesRepository.favoriteTimeZones, + ) { customUserTime, userTimeZone, selectedTimeZone, dialogState, favoriteTimeZones -> + return@combine TimeZoneUIState.Ready( + favorites = favoriteTimeZones, + customUserTime = customUserTime, + userTimeZone = userTimeZone, + selectedTimeZone = selectedTimeZone, + dialogState = dialogState ) - }.stateIn( - viewModelScope, SharingStarted.WhileSubscribed(5000), TimeZoneUIState() - ) + } + .stateIn(viewModelScope, TimeZoneUIState.Loading) - fun onDragEnd(from: String, to: String) = viewModelScope.launch { - timezonesRepository.swapTimeZones(from, to) + fun setCurrentTime() = _customUserTime.update { null } + + fun setSelectedTime(time: ZonedDateTime) = _customUserTime.update { time } + + fun setDialogState(state: TimeZoneDialogState) = _dialogState.update { state } + + fun onDragEnd( + tz: FavoriteZone, + targetPosition: Int + ) = viewModelScope.launch { + timezonesRepository.moveTimeZone(tz, targetPosition) } - fun onDelete(timeZone: UnittoTimeZone) = viewModelScope.launch { - timezonesRepository.delete(timeZone) + fun delete(timeZone: FavoriteZone) = viewModelScope.launch { + timezonesRepository.removeFromFavorites(timeZone) } - fun setCustomTime(time: ZonedDateTime) = viewModelScope.launch(Dispatchers.Default) { - _updateTime.update { false } - _userTime.update { time } - } + fun selectTimeZone(timeZone: FavoriteZone?) = _selectedTimeZone.update { timeZone } - fun resetTime() = viewModelScope.launch(Dispatchers.Default) { - _updateTime.update { true } - } - - fun setCurrentTime() = viewModelScope.launch(Dispatchers.Default) { - _userTime.update { ZonedDateTime.now() } + fun updateLabel( + timeZone: FavoriteZone, + label: String + ) = viewModelScope.launch { + timezonesRepository.updateLabel(timeZone = timeZone, label = label) } } 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 new file mode 100644 index 00000000..4a9fe390 --- /dev/null +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/FavoriteTimeZoneItem.kt @@ -0,0 +1,317 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.timezone.components + +import android.icu.util.TimeZone +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Schedule +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.base.R +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.model.timezone.FavoriteZone +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +@RequiresApi(Build.VERSION_CODES.N) +@Composable +internal fun FavoriteTimeZoneItem( + modifier: Modifier, + item: FavoriteZone, + fromTime: ZonedDateTime, + isDragging: Boolean, + expanded: Boolean, + onClick: () -> Unit, + onDelete: () -> Unit, + onLabelClick: () -> Unit, + onPrimaryClick: (ZonedDateTime) -> Unit, +) { + var deleteAnimationRunning by remember { mutableStateOf(false) } + val animatedAlpha by animateFloatAsState( + label = "delete animation", + targetValue = if (deleteAnimationRunning) 0f else 1f, + finishedListener = { if (it == 0f) onDelete() } + ) + + val offsetTime by remember(fromTime) { mutableStateOf(item.timeZone.offset(fromTime)) } + val offsetTimeFormatted = offsetTime.formatOffset(fromTime) + + Column( + modifier = Modifier + .graphicsLayer(alpha = animatedAlpha) + .then(modifier) + .clickable(enabled = !isDragging) { onClick() } + .padding(vertical = 16.dp, horizontal = 12.dp) + ) { + TimeZoneLabel( + label = item.label, + expanded = expanded, + onLabelClick = onLabelClick + ) + Row( + modifier = Modifier + .padding() + .heightIn(min = 56.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(2.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = item.timeZone.displayName, + style = MaterialTheme.typography.bodyLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + AnimatedVisibility( + visible = offsetTimeFormatted != null, + label = "Nullable offset" + ) { + Text( + text = offsetTimeFormatted ?: "", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + AnimatedContent( + targetState = offsetTime.formatLocal(), + label = "Time change", + transitionSpec = { + fadeIn() togetherWith fadeOut() using (SizeTransform(clip = false)) + } + ) { time -> + // TODO Add AM PM as dots (apply to 12 and 24 hour systems) + Text( + text = time, + style = MaterialTheme.typography.numberHeadlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + + AnimatedVisibility(visible = expanded) { + Column { + TimeZoneOption( + title = stringResource(R.string.select_time_label), + icon = Icons.Outlined.Schedule, + contentDescription = stringResource(R.string.select_time_label), + onClick = { onPrimaryClick(offsetTime) } + ) + TimeZoneOption( + title = stringResource(R.string.delete_label), + icon = Icons.Outlined.Delete, + contentDescription = stringResource(R.string.delete_label), + onClick = { deleteAnimationRunning = true } + ) + } + } + } +} + +@Composable +private fun TimeZoneOption( + title: String, + icon: ImageVector, + contentDescription: String, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable { onClick() } + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = title, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +private fun TimeZoneLabel( + label: String, + expanded: Boolean, + onLabelClick: () -> Unit, +) { + AnimatedContent( + targetState = label.isBlank(), + modifier = if (expanded) Modifier.clickable { onLabelClick() } else Modifier, + ) { blank -> + if (blank) { + AnimatedVisibility(expanded) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.add_label), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedVisibility(visible = expanded) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = label, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +private data class FavoriteTimeZoneItemParameter( + val expanded: Boolean, + val tz: FavoriteZone, +) + +@RequiresApi(Build.VERSION_CODES.N) +private class FavoriteTimeZoneItemParameterProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + FavoriteTimeZoneItemParameter( + expanded = false, + tz = FavoriteZone( + timeZone = TimeZone.getDefault(), + position = 1, + label = "" + ), + ), + FavoriteTimeZoneItemParameter( + expanded = false, + tz = FavoriteZone( + timeZone = TimeZone.getDefault(), + position = 1, + label = "Some text" + ), + ), + FavoriteTimeZoneItemParameter( + expanded = true, + tz = FavoriteZone( + timeZone = TimeZone.getDefault(), + position = 1, + label = "" + ), + ), + FavoriteTimeZoneItemParameter( + expanded = true, + tz = FavoriteZone( + timeZone = TimeZone.getDefault(), + position = 1, + label = "Some text" + ), + ) + ) +} + +@RequiresApi(Build.VERSION_CODES.N) +@Preview(showBackground = true, backgroundColor = 0xFFC1C9FF) +@Composable +private fun PreviewFavoriteTimeZones( + @PreviewParameter(FavoriteTimeZoneItemParameterProvider::class) tz: FavoriteTimeZoneItemParameter, +) { + var expanded by remember { mutableStateOf(tz.expanded) } + + FavoriteTimeZoneItem( + modifier = Modifier + .background(MaterialTheme.colorScheme.secondaryContainer), + item = tz.tz, + fromTime = ZonedDateTime.parse( + "2023-05-01T14:00+03:00[Africa/Addis_Ababa]", + DateTimeFormatter.ISO_ZONED_DATE_TIME + ), + expanded = expanded, + onClick = { expanded = !expanded }, + onDelete = {}, + onPrimaryClick = {}, + onLabelClick = {}, + isDragging = false + ) +} diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/SelectableTimeZone.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/SelectableTimeZone.kt deleted file mode 100644 index 24950a7d..00000000 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/SelectableTimeZone.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2023 Elshan Agaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sadellie.unitto.feature.timezone.components - -import androidx.compose.foundation.layout.width -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.sadellie.unitto.core.ui.datetime.formatLocal -import com.sadellie.unitto.data.model.UnittoTimeZone -import java.time.ZonedDateTime - -@Composable -fun SelectableTimeZone( - modifier: Modifier = Modifier, - timeZone: UnittoTimeZone, - currentTime: ZonedDateTime?, -) { - val offsetTime: ZonedDateTime? by remember(currentTime) { - mutableStateOf(currentTime?.let { timeZone.offsetFrom(it) }) - } - - ListItem( - modifier = modifier, - headlineContent = { - Text(text = timeZone.nameRes) - }, - supportingContent = { - Text(text = timeZone.id) - }, - trailingContent = { - Text(text = offsetTime?.formatLocal() ?: "", style = MaterialTheme.typography.headlineSmall) - } - ) -} - -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) -@Composable -fun PreviewSelectableTimeZone() { - SelectableTimeZone( - timeZone = UnittoTimeZone( - id = "text", - nameRes = "Time zone" - ), - modifier = Modifier.width(440.dp), - currentTime = ZonedDateTime.now(), - ) -} diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/TimeZoneListItem.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/TimeZoneListItem.kt deleted file mode 100644 index 1a2ef1a9..00000000 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/TimeZoneListItem.kt +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2023 Elshan Agaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sadellie.unitto.feature.timezone.components - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.SizeTransform -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import com.sadellie.unitto.core.ui.datetime.formatLocal -import com.sadellie.unitto.core.ui.datetime.formatOffset -import com.sadellie.unitto.data.model.UnittoTimeZone -import java.time.ZonedDateTime -import kotlin.math.roundToInt - -@Composable -fun TimeZoneListItem( - modifier: Modifier, - timeZone: UnittoTimeZone, - currentTime: ZonedDateTime, - onClick: () -> Unit = {}, - onDelete: (UnittoTimeZone) -> Unit = {}, - color: Color = MaterialTheme.colorScheme.surfaceVariant, - onSwipe: (UnittoTimeZone) -> Unit = {}, - draggableState: AnchoredDraggableState, -) { - val offsetTime by remember(currentTime) { mutableStateOf(timeZone.offsetFrom(currentTime)) } - val offsetTimeFormatted = offsetTime.formatOffset(currentTime) - - // TODO Animate deleting - Box( - modifier = modifier - .heightIn(72.dp) - .clip(RoundedCornerShape(25)) - .anchoredDraggable( - state = draggableState, - orientation = Orientation.Horizontal, - ), - contentAlignment = Alignment.Center, - ) { - // TODO Reveal animation - Box( - modifier = Modifier - .clickable { onDelete(timeZone) } - .background(MaterialTheme.colorScheme.errorContainer) - .matchParentSize() - ) { - Icon( - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(horizontal = 24.dp) - .size(32.dp), - imageVector = Icons.Outlined.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.onErrorContainer - ) - } - - Row( - modifier = Modifier - .offset { - IntOffset( - y = 0, - x = draggableState - .requireOffset() - .roundToInt() - ) - } - .clickable {} - .background(color) - .matchParentSize() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column(Modifier.weight(1f)) { - // TODO Name - Text( - text = timeZone.nameRes, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - AnimatedVisibility( - visible = offsetTimeFormatted != null, - label = "Nullable offset" - ) { - Text(offsetTimeFormatted ?: "", style = MaterialTheme.typography.bodyMedium) - } - } - Column { - // Time - AnimatedContent( - targetState = offsetTime.formatLocal(), - label = "Time change", - transitionSpec = { - fadeIn() togetherWith fadeOut() using (SizeTransform(clip = false)) - } - ) { time -> - Text(time, style = MaterialTheme.typography.headlineMedium) - } - } - } - } -} - -@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) -@Composable -fun PreviewTimeZoneListItem() { - - val maxDrag = -with(LocalDensity.current) { 80.dp.toPx() } - val draggableState = remember { - AnchoredDraggableState( - initialValue = false, - anchors = DraggableAnchors { - false at 0f - true at maxDrag - }, - positionalThreshold = { 0f }, - velocityThreshold = { 0f }, - animationSpec = tween(), - confirmValueChange = { true } - ) - } - - TimeZoneListItem( - modifier = Modifier, - timeZone = UnittoTimeZone( - id = "timezone", - offsetSeconds = -10800, - nameRes = "Time zone" - ), - currentTime = ZonedDateTime.now(), - draggableState = draggableState - ) -} diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/UserTimeZone.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/UserTimeZone.kt new file mode 100644 index 00000000..628e3e97 --- /dev/null +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/components/UserTimeZone.kt @@ -0,0 +1,161 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.timezone.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.History +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.squashable +import com.sadellie.unitto.core.ui.datetime.UnittoDateTimeFormatter +import com.sadellie.unitto.core.ui.datetime.formatOnlyHours +import com.sadellie.unitto.core.ui.datetime.formatOnlyMinutes +import com.sadellie.unitto.core.ui.theme.numberBodyLarge +import com.sadellie.unitto.core.ui.theme.numberDisplayLarge +import java.time.ZonedDateTime + +@Composable +internal fun UserTimeZone( + modifier: Modifier, + userTime: ZonedDateTime, + onClick: () -> Unit, + onResetClick: () -> Unit, + showReset: Boolean, +) { + + Row( + modifier = modifier + .squashable( + onClick = onClick, + onLongClick = onResetClick, + cornerRadiusRange = 8.dp..32.dp, + interactionSource = remember { MutableInteractionSource() } + ) + .background(MaterialTheme.colorScheme.tertiaryContainer) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Column(Modifier.weight(1f)) { + Text( + text = userTime.format(UnittoDateTimeFormatter.zoneFormatPattern), + style = MaterialTheme.typography.numberBodyLarge, + color = MaterialTheme.colorScheme.onTertiaryContainer + ) + + Row( + verticalAlignment = Alignment.Bottom + ) { + SlidingText(text = userTime.formatOnlyHours()) + TimeSeparator() + SlidingText(text = userTime.formatOnlyMinutes()) + } + + Text( + text = userTime.format(UnittoDateTimeFormatter.dayMonthYear), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer + ) + } + AnimatedVisibility( + visible = showReset, + enter = scaleIn() + fadeIn(), + exit = scaleOut() + fadeOut(), + ) { + IconButton(onResetClick) { + Icon( + imageVector = Icons.Outlined.History, + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiaryContainer, + ) + } + } + } +} + +@Composable +private fun SlidingText( + text: String, + style: TextStyle = MaterialTheme.typography.numberDisplayLarge +) { + AnimatedContent( + targetState = text, + label = "user time change", + transitionSpec = { + slideInVertically { height -> height } + fadeIn() togetherWith + slideOutVertically { height -> -height } + fadeOut() using + SizeTransform() + } + ) { target -> + Text( + text = target, + style = style, + color = MaterialTheme.colorScheme.onTertiaryContainer, + overflow = TextOverflow.Visible, + maxLines = 1 + ) + } +} + +@Composable +private fun TimeSeparator( + text: String = ":", + style: TextStyle = MaterialTheme.typography.numberDisplayLarge +) { + Text( + text = text, + style = style, + color = MaterialTheme.colorScheme.onTertiaryContainer + ) +} + +@Preview +@Composable +private fun PreviewUserTimeZone() { + UserTimeZone( + modifier = Modifier, + userTime = ZonedDateTime.now(), + onClick = {}, + onResetClick = {}, + showReset = true + ) +} diff --git a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/navigation/TimeZoneNavigation.kt b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/navigation/TimeZoneNavigation.kt index a1bf34d1..0d15f5d1 100644 --- a/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/navigation/TimeZoneNavigation.kt +++ b/feature/timezone/src/main/java/com/sadellie/unitto/feature/timezone/navigation/TimeZoneNavigation.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.feature.timezone.navigation +import android.os.Build import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController @@ -25,6 +26,7 @@ import androidx.navigation.NavType import androidx.navigation.navArgument import androidx.navigation.navDeepLink import com.sadellie.unitto.core.base.TopLevelDestinations +import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen import com.sadellie.unitto.core.ui.unittoComposable import com.sadellie.unitto.core.ui.unittoNavigation import com.sadellie.unitto.feature.timezone.AddTimeZoneRoute @@ -38,11 +40,11 @@ private const val ADD_TIME_ZONE_ROUTE = "ADD_TIME_ZONE_ROUTE" private const val USER_TIME_ARG = "USER_TIME_ARG" private fun NavController.navigateToAddTimeZone( - userTime: ZonedDateTime? + userTime: ZonedDateTime ) { val formattedTime = userTime - ?.format(DateTimeFormatter.ISO_ZONED_DATE_TIME) - ?.replace("/", "|") // this is so wrong + .format(DateTimeFormatter.ISO_ZONED_DATE_TIME) + .replace("/", "|") // this is so wrong navigate("$ADD_TIME_ZONE_ROUTE/$formattedTime") } @@ -60,8 +62,13 @@ fun NavGraphBuilder.timeZoneGraph( ) ) { unittoComposable(start) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + UnittoEmptyScreen() + return@unittoComposable + } + TimeZoneRoute( - navigateToMenu = navigateToMenu, + openMenu = navigateToMenu, navigateToSettings = navigateToSettings, navigateToAddTimeZone = navController::navigateToAddTimeZone ) @@ -77,10 +84,16 @@ fun NavGraphBuilder.timeZoneGraph( } ) ) { stackEntry -> + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + UnittoEmptyScreen() + return@unittoComposable + } + val userTime = stackEntry.arguments ?.getString(USER_TIME_ARG) ?.replace("|", "/") // war crime, don't look ?.let { ZonedDateTime.parse(it, DateTimeFormatter.ISO_ZONED_DATE_TIME) } + ?: ZonedDateTime.now() AddTimeZoneRoute( navigateUp = navController::navigateUp,