Added Time zone converter

Hidden as "tools experiment"

Missing:
- Clean data
- Translations
- UI/UX clean up
- Relative time change functionality
- Custom user time zone functionality

closes #59

this is a squashed commit
This commit is contained in:
sadellie 2023-07-22 18:30:09 +03:00
parent bba2f24c7c
commit caf025778e
41 changed files with 4813 additions and 2351 deletions

View File

@ -72,11 +72,19 @@ internal fun UnittoApp(uiPrefs: UIPreferences) {
// Navigation drawer stuff
val drawerState = rememberUnittoDrawerState()
val drawerScope = rememberCoroutineScope()
val mainTabs = listOf(
DrawerItems.Calculator,
DrawerItems.Converter,
DrawerItems.DateDifference
val mainTabs by remember(uiPrefs.enableToolsExperiment) {
derivedStateOf {
if (uiPrefs.enableToolsExperiment) {
listOf(
DrawerItems.Calculator, DrawerItems.Converter, DrawerItems.DateDifference, DrawerItems.TimeZones
)
} else {
listOf(
DrawerItems.Calculator, DrawerItems.Converter, DrawerItems.DateDifference,
)
}
}
}
val additionalTabs = listOf(DrawerItems.Settings)
val navBackStackEntry by navController.currentBackStackEntryAsState()
@ -129,7 +137,7 @@ internal fun UnittoApp(uiPrefs: UIPreferences) {
UnittoNavigation(
navController = navController,
themmoController = it,
startDestination = uiPrefs.startingScreen,
startDestination = TopLevelDestinations.TimeZone.route,
openDrawer = { drawerScope.launch { drawerState.open() } }
)
}

View File

@ -95,7 +95,8 @@ internal fun UnittoNavigation(
timeZoneScreen(
navigateToMenu = openDrawer,
navigateToSettings = navController::navigateToSettings
navigateToSettings = navController::navigateToSettings,
navController = navController,
)
}
}

View File

@ -40,8 +40,8 @@ sealed class TopLevelDestinations(
)
object TimeZone : TopLevelDestinations(
route = "time_zone_route",
name = R.string.time_zone
route = "time_zone_graph",
name = R.string.time_zone_screen
)
object Settings : TopLevelDestinations(
@ -54,5 +54,6 @@ val TOP_LEVEL_DESTINATIONS: Map<TopLevelDestinations, Int> by lazy {
mapOf(
TopLevelDestinations.Converter to R.string.unit_converter,
TopLevelDestinations.Calculator to R.string.calculator,
TopLevelDestinations.DateDifference to R.string.date_difference,
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,7 @@ android {
dependencies {
testImplementation(libs.junit)
testImplementation(libs.org.robolectric)
testImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)

View File

@ -18,6 +18,7 @@
package com.sadellie.unitto.core.ui.common
import android.content.res.Configuration
import android.text.format.DateFormat
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -44,6 +45,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
@ -59,23 +61,25 @@ import kotlin.math.max
@Composable
fun TimePickerDialog(
modifier: Modifier = Modifier,
localDateTime: LocalDateTime,
hour: Int,
minute: Int,
confirmLabel: String = stringResource(R.string.ok_label),
dismissLabel: String = stringResource(R.string.cancel_label),
onDismiss: () -> Unit = {},
onConfirm: (LocalDateTime) -> Unit,
vertical: Boolean
onConfirm: (hour: Int, minute: Int) -> Unit,
) {
val isVertical = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
val pickerState = rememberTimePickerState(
localDateTime.hour,
localDateTime.minute,
DateFormat.is24HourFormat(LocalContext.current)
initialHour = hour,
initialMinute = minute,
is24Hour = DateFormat.is24HourFormat(LocalContext.current)
)
AlertDialog(
onDismissRequest = onDismiss,
modifier = modifier.wrapContentHeight(),
properties = DialogProperties(usePlatformDefaultWidth = vertical)
properties = DialogProperties(usePlatformDefaultWidth = isVertical)
) {
Surface(
modifier = modifier,
@ -97,7 +101,7 @@ fun TimePickerDialog(
TimePicker(
state = pickerState,
modifier = Modifier.padding(top = 20.dp),
layoutType = if (vertical) TimePickerLayoutType.Vertical else TimePickerLayoutType.Horizontal
layoutType = if (isVertical) TimePickerLayoutType.Vertical else TimePickerLayoutType.Horizontal
)
Row(
@ -110,13 +114,7 @@ fun TimePickerDialog(
Text(text = dismissLabel)
}
TextButton(
onClick = {
onConfirm(
localDateTime
.withHour(pickerState.hour)
.withMinute(pickerState.minute)
)
}
onClick = { onConfirm(pickerState.hour, pickerState.minute) }
) {
Text(text = confirmLabel)
}

View File

@ -21,9 +21,11 @@ package com.sadellie.unitto.core.ui.common
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -35,6 +37,8 @@ import androidx.compose.ui.Modifier
* @param navigationIcon See [CenterAlignedTopAppBar]
* @param actions See [CenterAlignedTopAppBar]
* @param colors See [CenterAlignedTopAppBar]
* @param floatingActionButton See [Scaffold]
* @param scrollBehavior See [CenterAlignedTopAppBar]
* @param content See [Scaffold]
*/
@Composable
@ -44,6 +48,9 @@ fun UnittoScreenWithTopBar(
navigationIcon: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
scrollBehavior: TopAppBarScrollBehavior? = null,
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
@ -53,9 +60,12 @@ fun UnittoScreenWithTopBar(
title = title,
navigationIcon = navigationIcon,
actions = actions,
colors = colors
colors = colors,
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
content = content
)
}

View File

@ -0,0 +1,27 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.core.ui.datetime
import java.time.format.DateTimeFormatter
// FIXME Duplicate from date difference
internal val time24Formatter by lazy { DateTimeFormatter.ofPattern("HH:mm") }
internal val time12Formatter by lazy { DateTimeFormatter.ofPattern("hh:mm a") }
internal val dayMonthYear by lazy { DateTimeFormatter.ofPattern("d MMM y") }
internal val zoneFormatPattern by lazy { DateTimeFormatter.ofPattern("O") }

View File

@ -0,0 +1,48 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.core.ui.datetime
import android.text.format.DateFormat
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import java.time.LocalDateTime
@Composable
fun LocalDateTime.formatLocal(): String {
return if (DateFormat.is24HourFormat(LocalContext.current)) format24()
else format12()
}
/**
* Formats [LocalDateTime] into string that looks like
*
* 23:58
*
* @return Formatted string.
*/
fun LocalDateTime.format24(): String = this.format(time24Formatter)
/**
* Formats [LocalDateTime] into string that looks like
*
* 11:58 am
*
* @return Formatted string.
*/
fun LocalDateTime.format12(): String = this.format(time12Formatter)

View File

@ -0,0 +1,120 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.core.ui.datetime
import android.text.format.DateFormat
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.core.base.R
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
@Composable
fun ZonedDateTime.formatLocal(): String {
return if (DateFormat.is24HourFormat(LocalContext.current)) format24()
else format12()
}
/**
* Formats [ZonedDateTime] into string that looks like
*
* 23:58
*
* @return Formatted string.
*/
fun ZonedDateTime.format24(): String = this.format(time24Formatter)
/**
* Formats [ZonedDateTime] into string that looks like
*
* 11:58 am
*
* @return Formatted string.
*/
fun ZonedDateTime.format12(): String = this.format(time12Formatter)
/**
* Formats [ZonedDateTime] into string that looks like
*
* 21 Jul 2023
*
* @return Formatted string.
*/
fun ZonedDateTime.formatDayMonthYear(): String = this.format(dayMonthYear)
fun ZonedDateTime.formatTimeZoneOffset(): String = this.format(zoneFormatPattern)
/**
* Format offset string. Examples:
*
* 0
*
* +8
*
* +8, tomorrow
*
* -8, yesterday
*
* @receiver [ZonedDateTime] Time with offset.
* @param currentTime Time without offset.
* @return Formatted string.
*/
@Composable
fun ZonedDateTime.formatOffset(
currentTime: ZonedDateTime
): String? {
val offsetFixed = ChronoUnit.SECONDS.between(currentTime, this)
if (offsetFixed == 0L) return null
var resultBuffer = ""
val absoluteOffset = offsetFixed.absoluteValue
// Add a positive/negative prefix symbol
when {
offsetFixed > 0 -> resultBuffer += "+"
offsetFixed < 0 -> resultBuffer += "-"
}
// Formatted hours and minutes
val hour = absoluteOffset / 3600
val minute = absoluteOffset % 3600 / 60
if (hour != 0L) {
resultBuffer += "${hour}${stringResource(R.string.hour_short)}"
}
// TODO Very ugly
if (minute != 0L) {
if (hour != 0L) resultBuffer += " "
resultBuffer += "${minute}${stringResource(R.string.minute_short)}"
}
// Day after time string
val diff = this.dayOfYear - currentTime.dayOfYear
when {
diff > 0 -> resultBuffer += ", ${stringResource(R.string.tomorrow).lowercase()}"
diff < 0 -> resultBuffer += ", ${stringResource(R.string.yesterday).lowercase()}"
}
return resultBuffer
}

View File

@ -21,10 +21,12 @@ package com.sadellie.unitto.core.ui.model
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Calculate
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.outlined.Calculate
import androidx.compose.material.icons.outlined.Event
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.ui.graphics.vector.ImageVector
@ -53,6 +55,12 @@ sealed class DrawerItems(
defaultIcon = Icons.Outlined.Event
)
object TimeZones : DrawerItems(
destination = TopLevelDestinations.TimeZone,
selectedIcon = Icons.Filled.Schedule,
defaultIcon = Icons.Outlined.Schedule
)
object Settings : DrawerItems(
destination = TopLevelDestinations.Settings,
selectedIcon = Icons.Filled.Settings,

View File

@ -27,14 +27,14 @@ import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.sadellie.unitto.core.base.R
private val Montserrat = FontFamily(
val Montserrat = FontFamily(
Font(R.font.montserrat_light, weight = FontWeight.Light),
Font(R.font.montserrat_regular, weight = FontWeight.Normal),
Font(R.font.montserrat_medium, weight = FontWeight.Medium),
Font(R.font.montserrat_semibold, weight = FontWeight.SemiBold),
)
private val Lato = FontFamily(
val Lato = FontFamily(
Font(R.font.lato_regular)
)

View File

@ -0,0 +1,176 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.core.ui
import androidx.compose.ui.test.junit4.createComposeRule
import com.sadellie.unitto.core.ui.datetime.formatOffset
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.assertEquals
import org.robolectric.RobolectricTestRunner
import java.time.ZonedDateTime
@RunWith(RobolectricTestRunner::class)
class ZonedDateTimeUtilsTest {
@get: Rule
val composeTestRule = createComposeRule()
@Test
fun `no difference`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val formatted = currentTime.formatOffset(currentTime)
assertEquals(null, formatted)
}
@Test
fun `show positive hour`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.plusSeconds(7200) // + 2h = 14:00
.formatOffset(currentTime)
assertEquals("+2h", offset)
}
@Test
fun `show positive hour minute`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.plusSeconds(9000) // + 2h 30m = 14:30
.formatOffset(currentTime)
assertEquals("+2h 30m", offset)
}
@Test
fun `show positive hour minute day`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.plusSeconds(50400) // + 14h = 02:00 tomorrow
.formatOffset(currentTime)
assertEquals("+14h, tomorrow", offset)
}
@Test
fun `show positive minute`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.plusSeconds(1800) // + 30m = 12:30
.formatOffset(currentTime)
assertEquals("+30m", offset)
}
@Test
fun `show positive minute day`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(23)
.withMinute(45)
val offset = currentTime
.plusSeconds(1800) // + 30m = 00:15 tomorrow
.formatOffset(currentTime)
assertEquals("+30m, tomorrow", offset)
}
@Test
fun `show negative hour`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.minusSeconds(7200) // - 2h = 10:00
.formatOffset(currentTime)
assertEquals("-2h", offset)
}
@Test
fun `show negative hour minute`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.minusSeconds(9000) // - 2h 30m = 09:30 tomorrow
.formatOffset(currentTime)
assertEquals("-2h 30m", offset)
}
@Test
fun `show negative hour minute day`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.minusSeconds(50400) // - 14h = 22:00 yesterday
.formatOffset(currentTime)
assertEquals("-14h, yesterday", offset)
}
@Test
fun `show negative minute`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(12)
.withMinute(0)
val offset = currentTime
.minusSeconds(1800) // - 30m = 11:30
.formatOffset(currentTime)
assertEquals("-30m", offset)
}
@Test
fun `show negative minute day`() = composeTestRule.setContent {
val currentTime = ZonedDateTime.now()
.withHour(0)
.withMinute(15)
val offset = currentTime
.minusSeconds(1800) // - 30m = 23:45 yesterday
.formatOffset(currentTime)
assertEquals("-30m, yesterday", offset)
}
}

View File

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "d5dca9e0346c3400b7ff5b31e85c7827",
"identityHash": "ab71572ff5556256d1f042af36243f6f",
"entities": [
{
"tableName": "units",
@ -34,10 +34,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"unitId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -72,10 +72,42 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"entityId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "time_zones",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": true
}
],
"autoGenerate": true
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
@ -84,7 +116,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd5dca9e0346c3400b7ff5b31e85c7827')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ab71572ff5556256d1f042af36243f6f')"
]
}
}

View File

@ -0,0 +1,122 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "ab71572ff5556256d1f042af36243f6f",
"entities": [
{
"tableName": "units",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`unitId` TEXT NOT NULL, `is_favorite` INTEGER, `paired_unit_id` TEXT, `frequency` INTEGER, PRIMARY KEY(`unitId`))",
"fields": [
{
"fieldPath": "unitId",
"columnName": "unitId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isFavorite",
"columnName": "is_favorite",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "pairedUnitId",
"columnName": "paired_unit_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "frequency",
"columnName": "frequency",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"unitId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "calculator_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`entityId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `expression` TEXT NOT NULL, `result` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "entityId",
"columnName": "entityId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expression",
"columnName": "expression",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "result",
"columnName": "result",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"entityId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "time_zones",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ab71572ff5556256d1f042af36243f6f')"
]
}
}

View File

@ -0,0 +1,41 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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 kotlinx.coroutines.flow.Flow
@Dao
interface TimeZoneDao {
@Query("SELECT * FROM time_zones ORDER BY position ASC")
fun getAll(): Flow<List<TimeZoneEntity>>
@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)
@Delete
suspend fun remove(timeZoneEntity: TimeZoneEntity)
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.data.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "time_zones")
class TimeZoneEntity(
@PrimaryKey val id: String,
@ColumnInfo(name = "position") val position: Int,
@ColumnInfo(name = "label") val label: String = "",
)

View File

@ -23,17 +23,20 @@ import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
version = 2,
version = 3,
exportSchema = true,
entities = [
UnitsEntity::class,
CalculatorHistoryEntity::class
CalculatorHistoryEntity::class,
TimeZoneEntity::class
],
autoMigrations = [
AutoMigration (from = 1, to = 2)
AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3),
]
)
abstract class UnittoDatabase : RoomDatabase() {
abstract fun unitsDao(): UnitsDao
abstract fun calculatorHistoryDao(): CalculatorHistoryDao
abstract fun timeZoneDao(): TimeZoneDao
}

View File

@ -56,6 +56,17 @@ class UnittoDatabaseModule {
return unittoDatabase.calculatorHistoryDao()
}
/**
* Tells Hilt to use this method to get [TimeZoneDao]
*
* @param unittoDatabase Database for which we need DAO
* @return Singleton of [TimeZoneDao]
*/
@Provides
fun provideTimeZoneDao(unittoDatabase: UnittoDatabase): TimeZoneDao {
return unittoDatabase.timeZoneDao()
}
/**
* Tells Hilt to use this method to get [UnittoDatabase]
*

View File

@ -0,0 +1,37 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.data.model
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)
return currentTimeWithoutOffset.plusSeconds(this.offsetSeconds)
}
}

1
data/timezone/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,33 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
plugins {
id("unitto.library")
id("unitto.android.hilt")
}
android {
namespace = "com.sadellie.unitto.data.timezone"
}
dependencies {
implementation(project(mapOf("path" to ":core:base")))
implementation(project(mapOf("path" to ":data:common")))
implementation(project(mapOf("path" to ":data:model")))
implementation(project(mapOf("path" to ":data:database")))
}

View File

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<manifest>
</manifest>

View File

@ -0,0 +1,490 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.data.timezone
import com.sadellie.unitto.data.common.lev
import com.sadellie.unitto.data.model.UnittoTimeZone
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class TimeZonesRepository @Inject constructor() {
private val allTimeZones: HashMap<String, UnittoTimeZone> = hashMapOf(
"alfa_time_zone" to UnittoTimeZone(id = "alfa_time_zone", nameRes = "Alfa Time Zone", offsetSeconds = 3600),
"australian_central_daylight_time" to UnittoTimeZone(id = "australian_central_daylight_time", nameRes = "Australian Central Daylight Time", offsetSeconds = 37800),
"australian_central_standard_time" to UnittoTimeZone(id = "australian_central_standard_time", nameRes = "Australian Central Standard Time", offsetSeconds = 34200),
"acre_time" to UnittoTimeZone(id = "acre_time", nameRes = "Acre Time", offsetSeconds = -18000),
"australian_central_western_standard_time" to UnittoTimeZone(id = "australian_central_western_standard_time", nameRes = "Australian Central Western Standard Time", offsetSeconds = 31500),
"arabia_daylight_time" to UnittoTimeZone(id = "arabia_daylight_time", nameRes = "Arabia Daylight Time", offsetSeconds = 14400),
"atlantic_daylight_time" to UnittoTimeZone(id = "atlantic_daylight_time", nameRes = "Atlantic Daylight Time", offsetSeconds = -10800),
"heure_avanc_e_de_latlantique_french" to UnittoTimeZone(id = "heure_avanc_e_de_latlantique_french", nameRes = "Heure Avanc-e de l'Atlantique (French)", offsetSeconds = -10800),
"australian_eastern_daylight_time" to UnittoTimeZone(id = "australian_eastern_daylight_time", nameRes = "Australian Eastern Daylight Time", offsetSeconds = 39600),
"eastern_daylight_time" to UnittoTimeZone(id = "eastern_daylight_time", nameRes = "Eastern Daylight Time", offsetSeconds = 39600),
"eastern_daylight_saving_time" to UnittoTimeZone(id = "eastern_daylight_saving_time", nameRes = "Eastern Daylight Saving Time", offsetSeconds = 39600),
"australian_eastern_standard_time" to UnittoTimeZone(id = "australian_eastern_standard_time", nameRes = "Australian Eastern Standard Time", offsetSeconds = 36000),
"eastern_standard_time" to UnittoTimeZone(id = "eastern_standard_time", nameRes = "Eastern Standard Time", offsetSeconds = 36000),
"australian_eastern_time" to UnittoTimeZone(id = "australian_eastern_time", nameRes = "Australian Eastern Time", offsetSeconds = 36000),
"afghanistan_time" to UnittoTimeZone(id = "afghanistan_time", nameRes = "Afghanistan Time", offsetSeconds = 16200),
"alaska_daylight_time" to UnittoTimeZone(id = "alaska_daylight_time", nameRes = "Alaska Daylight Time", offsetSeconds = -28800),
"alaska_standard_time" to UnittoTimeZone(id = "alaska_standard_time", nameRes = "Alaska Standard Time", offsetSeconds = -32400),
"alma_ata_time" to UnittoTimeZone(id = "alma_ata_time", nameRes = "Alma-Ata Time", offsetSeconds = 21600),
"amazon_summer_time" to UnittoTimeZone(id = "amazon_summer_time", nameRes = "Amazon Summer Time", offsetSeconds = -10800),
"armenia_daylight_time" to UnittoTimeZone(id = "armenia_daylight_time", nameRes = "Armenia Daylight Time", offsetSeconds = 18000),
"amazon_time" to UnittoTimeZone(id = "amazon_time", nameRes = "Amazon Time", offsetSeconds = -14400),
"armenia_time" to UnittoTimeZone(id = "armenia_time", nameRes = "Armenia Time", offsetSeconds = 14400),
"anadyr_summer_time" to UnittoTimeZone(id = "anadyr_summer_time", nameRes = "Anadyr Summer Time", offsetSeconds = 43200),
"anadyr_time" to UnittoTimeZone(id = "anadyr_time", nameRes = "Anadyr Time", offsetSeconds = 43200),
"aqtobe_time" to UnittoTimeZone(id = "aqtobe_time", nameRes = "Aqtobe Time", offsetSeconds = 18000),
"argentina_time" to UnittoTimeZone(id = "argentina_time", nameRes = "Argentina Time", offsetSeconds = -10800),
"arabic_standard_time" to UnittoTimeZone(id = "arabic_standard_time", nameRes = "Arabic Standard Time", offsetSeconds = 10800),
"atlantic_standard_time" to UnittoTimeZone(id = "atlantic_standard_time", nameRes = "Atlantic Standard Time", offsetSeconds = -14400),
"tiempo_est_ndar_del_atl_ntico_spanish" to UnittoTimeZone(id = "tiempo_est_ndar_del_atl_ntico_spanish", nameRes = "Tiempo Est-ndar del Atl-ntico (Spanish)", offsetSeconds = -14400),
"heure_normale_de_latlantique_french" to UnittoTimeZone(id = "heure_normale_de_latlantique_french", nameRes = "Heure Normale de l'Atlantique (French)", offsetSeconds = -14400),
"australian_western_daylight_time" to UnittoTimeZone(id = "australian_western_daylight_time", nameRes = "Australian Western Daylight Time", offsetSeconds = 32400),
"western_daylight_time" to UnittoTimeZone(id = "western_daylight_time", nameRes = "Western Daylight Time", offsetSeconds = 32400),
"western_summer_time" to UnittoTimeZone(id = "western_summer_time", nameRes = "Western Summer Time", offsetSeconds = 32400),
"australian_western_standard_time" to UnittoTimeZone(id = "australian_western_standard_time", nameRes = "Australian Western Standard Time", offsetSeconds = 28800),
"western_standard_time" to UnittoTimeZone(id = "western_standard_time", nameRes = "Western Standard Time", offsetSeconds = 28800),
"western_australia_time" to UnittoTimeZone(id = "western_australia_time", nameRes = "Western Australia Time", offsetSeconds = 28800),
"azores_summer_time" to UnittoTimeZone(id = "azores_summer_time", nameRes = "Azores Summer Time", offsetSeconds = 0),
"azores_daylight_time" to UnittoTimeZone(id = "azores_daylight_time", nameRes = "Azores Daylight Time", offsetSeconds = 0),
"azores_time" to UnittoTimeZone(id = "azores_time", nameRes = "Azores Time", offsetSeconds = -3600),
"azores_standard_time" to UnittoTimeZone(id = "azores_standard_time", nameRes = "Azores Standard Time", offsetSeconds = -3600),
"azerbaijan_summer_time" to UnittoTimeZone(id = "azerbaijan_summer_time", nameRes = "Azerbaijan Summer Time", offsetSeconds = 18000),
"azerbaijan_time" to UnittoTimeZone(id = "azerbaijan_time", nameRes = "Azerbaijan Time", offsetSeconds = 14400),
"anywhere_on_earth" to UnittoTimeZone(id = "anywhere_on_earth", nameRes = "Anywhere on Earth", offsetSeconds = -43200),
"bravo_time_zone" to UnittoTimeZone(id = "bravo_time_zone", nameRes = "Bravo Time Zone", offsetSeconds = 7200),
"brunei_darussalam_time" to UnittoTimeZone(id = "brunei_darussalam_time", nameRes = "Brunei Darussalam Time", offsetSeconds = 28800),
"brunei_time" to UnittoTimeZone(id = "brunei_time", nameRes = "Brunei Time", offsetSeconds = 28800),
"bolivia_time" to UnittoTimeZone(id = "bolivia_time", nameRes = "Bolivia Time", offsetSeconds = -14400),
"brasilia_summer_time" to UnittoTimeZone(id = "brasilia_summer_time", nameRes = "Brasilia Summer Time", offsetSeconds = -7200),
"brazil_summer_time" to UnittoTimeZone(id = "brazil_summer_time", nameRes = "Brazil Summer Time", offsetSeconds = -7200),
"brazilian_summer_time" to UnittoTimeZone(id = "brazilian_summer_time", nameRes = "Brazilian Summer Time", offsetSeconds = -7200),
"bras_lia_time" to UnittoTimeZone(id = "bras_lia_time", nameRes = "Bras-lia Time", offsetSeconds = -10800),
"brazil_time" to UnittoTimeZone(id = "brazil_time", nameRes = "Brazil Time", offsetSeconds = -10800),
"brazilian_time" to UnittoTimeZone(id = "brazilian_time", nameRes = "Brazilian Time", offsetSeconds = -10800),
"bangladesh_standard_time" to UnittoTimeZone(id = "bangladesh_standard_time", nameRes = "Bangladesh Standard Time", offsetSeconds = 21600),
"bougainville_standard_time" to UnittoTimeZone(id = "bougainville_standard_time", nameRes = "Bougainville Standard Time", offsetSeconds = 39600),
"british_summer_time" to UnittoTimeZone(id = "british_summer_time", nameRes = "British Summer Time", offsetSeconds = 3600),
"british_daylight_time" to UnittoTimeZone(id = "british_daylight_time", nameRes = "British Daylight Time", offsetSeconds = 3600),
"british_daylight_saving_time" to UnittoTimeZone(id = "british_daylight_saving_time", nameRes = "British Daylight Saving Time", offsetSeconds = 3600),
"bhutan_time" to UnittoTimeZone(id = "bhutan_time", nameRes = "Bhutan Time", offsetSeconds = 21600),
"charlie_time_zone" to UnittoTimeZone(id = "charlie_time_zone", nameRes = "Charlie Time Zone", offsetSeconds = 10800),
"casey_time" to UnittoTimeZone(id = "casey_time", nameRes = "Casey Time", offsetSeconds = 28800),
"central_africa_time" to UnittoTimeZone(id = "central_africa_time", nameRes = "Central Africa Time", offsetSeconds = 7200),
"cocos_islands_time" to UnittoTimeZone(id = "cocos_islands_time", nameRes = "Cocos Islands Time", offsetSeconds = 23400),
"central_daylight_time" to UnittoTimeZone(id = "central_daylight_time", nameRes = "Central Daylight Time", offsetSeconds = -18000),
"central_daylight_saving_time" to UnittoTimeZone(id = "central_daylight_saving_time", nameRes = "Central Daylight Saving Time", offsetSeconds = -18000),
"north_american_central_daylight_time" to UnittoTimeZone(id = "north_american_central_daylight_time", nameRes = "North American Central Daylight Time", offsetSeconds = -18000),
"heure_avanc_e_du_centre_french" to UnittoTimeZone(id = "heure_avanc_e_du_centre_french", nameRes = "Heure Avanc-e du Centre (French)", offsetSeconds = -18000),
"cuba_daylight_time" to UnittoTimeZone(id = "cuba_daylight_time", nameRes = "Cuba Daylight Time", offsetSeconds = -14400),
"central_european_summer_time" to UnittoTimeZone(id = "central_european_summer_time", nameRes = "Central European Summer Time", offsetSeconds = 7200),
"central_european_daylight_time" to UnittoTimeZone(id = "central_european_daylight_time", nameRes = "Central European Daylight Time", offsetSeconds = 7200),
"european_central_summer_time" to UnittoTimeZone(id = "european_central_summer_time", nameRes = "European Central Summer Time", offsetSeconds = 7200),
"mitteleurop_ische_sommerzeit_german" to UnittoTimeZone(id = "mitteleurop_ische_sommerzeit_german", nameRes = "Mitteleurop-ische Sommerzeit (German)", offsetSeconds = 7200),
"central_european_time" to UnittoTimeZone(id = "central_european_time", nameRes = "Central European Time", offsetSeconds = 3600),
"european_central_time" to UnittoTimeZone(id = "european_central_time", nameRes = "European Central Time", offsetSeconds = 3600),
"central_europe_time" to UnittoTimeZone(id = "central_europe_time", nameRes = "Central Europe Time", offsetSeconds = 3600),
"mitteleurop_ische_zeit_german" to UnittoTimeZone(id = "mitteleurop_ische_zeit_german", nameRes = "Mitteleurop-ische Zeit (German)", offsetSeconds = 3600),
"chatham_island_daylight_time" to UnittoTimeZone(id = "chatham_island_daylight_time", nameRes = "Chatham Island Daylight Time", offsetSeconds = 49500),
"chatham_daylight_time" to UnittoTimeZone(id = "chatham_daylight_time", nameRes = "Chatham Daylight Time", offsetSeconds = 49500),
"chatham_island_standard_time" to UnittoTimeZone(id = "chatham_island_standard_time", nameRes = "Chatham Island Standard Time", offsetSeconds = 45900),
"choibalsan_summer_time" to UnittoTimeZone(id = "choibalsan_summer_time", nameRes = "Choibalsan Summer Time", offsetSeconds = 32400),
"choibalsan_daylight_time" to UnittoTimeZone(id = "choibalsan_daylight_time", nameRes = "Choibalsan Daylight Time", offsetSeconds = 32400),
"choibalsan_daylight_saving_time" to UnittoTimeZone(id = "choibalsan_daylight_saving_time", nameRes = "Choibalsan Daylight Saving Time", offsetSeconds = 32400),
"choibalsan_time" to UnittoTimeZone(id = "choibalsan_time", nameRes = "Choibalsan Time", offsetSeconds = 28800),
"chuuk_time" to UnittoTimeZone(id = "chuuk_time", nameRes = "Chuuk Time", offsetSeconds = 36000),
"cayman_islands_daylight_saving_time" to UnittoTimeZone(id = "cayman_islands_daylight_saving_time", nameRes = "Cayman Islands Daylight Saving Time", offsetSeconds = -14400),
"cayman_islands_standard_time" to UnittoTimeZone(id = "cayman_islands_standard_time", nameRes = "Cayman Islands Standard Time", offsetSeconds = -18000),
"cayman_islands_time" to UnittoTimeZone(id = "cayman_islands_time", nameRes = "Cayman Islands Time", offsetSeconds = -18000),
"cook_island_time" to UnittoTimeZone(id = "cook_island_time", nameRes = "Cook Island Time", offsetSeconds = -36000),
"chile_summer_time" to UnittoTimeZone(id = "chile_summer_time", nameRes = "Chile Summer Time", offsetSeconds = -10800),
"chile_daylight_time" to UnittoTimeZone(id = "chile_daylight_time", nameRes = "Chile Daylight Time", offsetSeconds = -10800),
"chile_standard_time" to UnittoTimeZone(id = "chile_standard_time", nameRes = "Chile Standard Time", offsetSeconds = -14400),
"chile_time" to UnittoTimeZone(id = "chile_time", nameRes = "Chile Time", offsetSeconds = -14400),
"chile_standard_time" to UnittoTimeZone(id = "chile_standard_time", nameRes = "Chile Standard Time", offsetSeconds = -14400),
"colombia_time" to UnittoTimeZone(id = "colombia_time", nameRes = "Colombia Time", offsetSeconds = -18000),
"central_standard_time" to UnittoTimeZone(id = "central_standard_time", nameRes = "Central Standard Time", offsetSeconds = -21600),
"central_time" to UnittoTimeZone(id = "central_time", nameRes = "Central Time", offsetSeconds = -21600),
"north_american_central_standard_time" to UnittoTimeZone(id = "north_american_central_standard_time", nameRes = "North American Central Standard Time", offsetSeconds = -21600),
"tiempo_central_est_ndar_spanish" to UnittoTimeZone(id = "tiempo_central_est_ndar_spanish", nameRes = "Tiempo Central Est-ndar (Spanish)", offsetSeconds = -21600),
"heure_normale_du_centre_french" to UnittoTimeZone(id = "heure_normale_du_centre_french", nameRes = "Heure Normale du Centre (French)", offsetSeconds = -21600),
"china_standard_time" to UnittoTimeZone(id = "china_standard_time", nameRes = "China Standard Time", offsetSeconds = 28800),
"cuba_standard_time" to UnittoTimeZone(id = "cuba_standard_time", nameRes = "Cuba Standard Time", offsetSeconds = -18000),
"cape_verde_time" to UnittoTimeZone(id = "cape_verde_time", nameRes = "Cape Verde Time", offsetSeconds = -3600),
"christmas_island_time" to UnittoTimeZone(id = "christmas_island_time", nameRes = "Christmas Island Time", offsetSeconds = 25200),
"chamorro_standard_time" to UnittoTimeZone(id = "chamorro_standard_time", nameRes = "Chamorro Standard Time", offsetSeconds = 36000),
"guam_standard_time" to UnittoTimeZone(id = "guam_standard_time", nameRes = "Guam Standard Time", offsetSeconds = 36000),
"delta_time_zone" to UnittoTimeZone(id = "delta_time_zone", nameRes = "Delta Time Zone", offsetSeconds = 14400),
"davis_time" to UnittoTimeZone(id = "davis_time", nameRes = "Davis Time", offsetSeconds = 25200),
"dumont_durville_time" to UnittoTimeZone(id = "dumont_durville_time", nameRes = "Dumont-d'Urville Time", offsetSeconds = 36000),
"echo_time_zone" to UnittoTimeZone(id = "echo_time_zone", nameRes = "Echo Time Zone", offsetSeconds = 18000),
"easter_island_summer_time" to UnittoTimeZone(id = "easter_island_summer_time", nameRes = "Easter Island Summer Time", offsetSeconds = -18000),
"easter_island_daylight_time" to UnittoTimeZone(id = "easter_island_daylight_time", nameRes = "Easter Island Daylight Time", offsetSeconds = -18000),
"easter_island_standard_time" to UnittoTimeZone(id = "easter_island_standard_time", nameRes = "Easter Island Standard Time", offsetSeconds = -21600),
"eastern_africa_time" to UnittoTimeZone(id = "eastern_africa_time", nameRes = "Eastern Africa Time", offsetSeconds = 10800),
"east_africa_time" to UnittoTimeZone(id = "east_africa_time", nameRes = "East Africa Time", offsetSeconds = 10800),
"ecuador_time" to UnittoTimeZone(id = "ecuador_time", nameRes = "Ecuador Time", offsetSeconds = -18000),
"eastern_daylight_time" to UnittoTimeZone(id = "eastern_daylight_time", nameRes = "Eastern Daylight Time", offsetSeconds = -14400),
"eastern_daylight_savings_time" to UnittoTimeZone(id = "eastern_daylight_savings_time", nameRes = "Eastern Daylight Savings Time", offsetSeconds = -14400),
"north_american_eastern_daylight_time" to UnittoTimeZone(id = "north_american_eastern_daylight_time", nameRes = "North American Eastern Daylight Time", offsetSeconds = -14400),
"heure_avanc_e_de_lest_french" to UnittoTimeZone(id = "heure_avanc_e_de_lest_french", nameRes = "Heure Avanc-e de l'Est (French)", offsetSeconds = -14400),
"tiempo_de_verano_del_este_spanish" to UnittoTimeZone(id = "tiempo_de_verano_del_este_spanish", nameRes = "Tiempo de verano del Este (Spanish)", offsetSeconds = -14400),
"eastern_european_summer_time" to UnittoTimeZone(id = "eastern_european_summer_time", nameRes = "Eastern European Summer Time", offsetSeconds = 10800),
"eastern_european_daylight_time" to UnittoTimeZone(id = "eastern_european_daylight_time", nameRes = "Eastern European Daylight Time", offsetSeconds = 10800),
"osteurop_ische_sommerzeit_german" to UnittoTimeZone(id = "osteurop_ische_sommerzeit_german", nameRes = "Osteurop-ische Sommerzeit (German)", offsetSeconds = 10800),
"eastern_european_time" to UnittoTimeZone(id = "eastern_european_time", nameRes = "Eastern European Time", offsetSeconds = 7200),
"osteurop_ische_zeit_german" to UnittoTimeZone(id = "osteurop_ische_zeit_german", nameRes = "Osteurop-ische Zeit (German)", offsetSeconds = 7200),
"eastern_greenland_summer_time" to UnittoTimeZone(id = "eastern_greenland_summer_time", nameRes = "Eastern Greenland Summer Time", offsetSeconds = 0),
"east_greenland_summer_time" to UnittoTimeZone(id = "east_greenland_summer_time", nameRes = "East Greenland Summer Time", offsetSeconds = 0),
"east_greenland_time" to UnittoTimeZone(id = "east_greenland_time", nameRes = "East Greenland Time", offsetSeconds = -3600),
"eastern_greenland_time" to UnittoTimeZone(id = "eastern_greenland_time", nameRes = "Eastern Greenland Time", offsetSeconds = -3600),
"eastern_standard_time" to UnittoTimeZone(id = "eastern_standard_time", nameRes = "Eastern Standard Time", offsetSeconds = -18000),
"eastern_time_" to UnittoTimeZone(id = "eastern_time_", nameRes = "Eastern Time ", offsetSeconds = -18000),
"north_american_eastern_standard_time" to UnittoTimeZone(id = "north_american_eastern_standard_time", nameRes = "North American Eastern Standard Time", offsetSeconds = -18000),
"tiempo_del_este_spanish" to UnittoTimeZone(id = "tiempo_del_este_spanish", nameRes = "Tiempo del Este (Spanish)", offsetSeconds = -18000),
"heure_normale_de_lest_french" to UnittoTimeZone(id = "heure_normale_de_lest_french", nameRes = "Heure Normale de l'Est (French)", offsetSeconds = -18000),
"foxtrot_time_zone" to UnittoTimeZone(id = "foxtrot_time_zone", nameRes = "Foxtrot Time Zone", offsetSeconds = 21600),
"further_eastern_european_time" to UnittoTimeZone(id = "further_eastern_european_time", nameRes = "Further-Eastern European Time", offsetSeconds = 10800),
"fiji_summer_time" to UnittoTimeZone(id = "fiji_summer_time", nameRes = "Fiji Summer Time", offsetSeconds = 46800),
"fiji_daylight_time" to UnittoTimeZone(id = "fiji_daylight_time", nameRes = "Fiji Daylight Time", offsetSeconds = 46800),
"fiji_time" to UnittoTimeZone(id = "fiji_time", nameRes = "Fiji Time", offsetSeconds = 43200),
"falkland_islands_summer_time" to UnittoTimeZone(id = "falkland_islands_summer_time", nameRes = "Falkland Islands Summer Time", offsetSeconds = -10800),
"falkland_island_daylight_time" to UnittoTimeZone(id = "falkland_island_daylight_time", nameRes = "Falkland Island Daylight Time", offsetSeconds = -10800),
"falkland_island_time" to UnittoTimeZone(id = "falkland_island_time", nameRes = "Falkland Island Time", offsetSeconds = -14400),
"falkland_island_standard_time" to UnittoTimeZone(id = "falkland_island_standard_time", nameRes = "Falkland Island Standard Time", offsetSeconds = -14400),
"fernando_de_noronha_time" to UnittoTimeZone(id = "fernando_de_noronha_time", nameRes = "Fernando de Noronha Time", offsetSeconds = -7200),
"golf_time_zone" to UnittoTimeZone(id = "golf_time_zone", nameRes = "Golf Time Zone", offsetSeconds = 25200),
"galapagos_time" to UnittoTimeZone(id = "galapagos_time", nameRes = "Galapagos Time", offsetSeconds = -21600),
"gambier_time" to UnittoTimeZone(id = "gambier_time", nameRes = "Gambier Time", offsetSeconds = -32400),
"gambier_islands_time" to UnittoTimeZone(id = "gambier_islands_time", nameRes = "Gambier Islands Time", offsetSeconds = -32400),
"georgia_standard_time" to UnittoTimeZone(id = "georgia_standard_time", nameRes = "Georgia Standard Time", offsetSeconds = 14400),
"french_guiana_time" to UnittoTimeZone(id = "french_guiana_time", nameRes = "French Guiana Time", offsetSeconds = -10800),
"gilbert_island_time" to UnittoTimeZone(id = "gilbert_island_time", nameRes = "Gilbert Island Time", offsetSeconds = 43200),
"greenwich_mean_time" to UnittoTimeZone(id = "greenwich_mean_time", nameRes = "Greenwich Mean Time", offsetSeconds = 0),
"coordinated_universal_time" to UnittoTimeZone(id = "coordinated_universal_time", nameRes = "Coordinated Universal Time", offsetSeconds = 0),
"greenwich_time" to UnittoTimeZone(id = "greenwich_time", nameRes = "Greenwich Time", offsetSeconds = 0),
"gulf_standard_time" to UnittoTimeZone(id = "gulf_standard_time", nameRes = "Gulf Standard Time", offsetSeconds = 14400),
"south_georgia_time" to UnittoTimeZone(id = "south_georgia_time", nameRes = "South Georgia Time", offsetSeconds = -7200),
"guyana_time" to UnittoTimeZone(id = "guyana_time", nameRes = "Guyana Time", offsetSeconds = -14400),
"hotel_time_zone" to UnittoTimeZone(id = "hotel_time_zone", nameRes = "Hotel Time Zone", offsetSeconds = 28800),
"hawaii_aleutian_daylight_time" to UnittoTimeZone(id = "hawaii_aleutian_daylight_time", nameRes = "Hawaii-Aleutian Daylight Time", offsetSeconds = -32400),
"hawaii_daylight_time" to UnittoTimeZone(id = "hawaii_daylight_time", nameRes = "Hawaii Daylight Time", offsetSeconds = -32400),
"hong_kong_time" to UnittoTimeZone(id = "hong_kong_time", nameRes = "Hong Kong Time", offsetSeconds = 28800),
"hovd_summer_time" to UnittoTimeZone(id = "hovd_summer_time", nameRes = "Hovd Summer Time", offsetSeconds = 28800),
"hovd_daylight_time" to UnittoTimeZone(id = "hovd_daylight_time", nameRes = "Hovd Daylight Time", offsetSeconds = 28800),
"hovd_daylight_saving_time" to UnittoTimeZone(id = "hovd_daylight_saving_time", nameRes = "Hovd Daylight Saving Time", offsetSeconds = 28800),
"hovd_time" to UnittoTimeZone(id = "hovd_time", nameRes = "Hovd Time", offsetSeconds = 25200),
"hawaii_standard_time" to UnittoTimeZone(id = "hawaii_standard_time", nameRes = "Hawaii Standard Time", offsetSeconds = -36000),
"hawaii_aleutian_standard_time" to UnittoTimeZone(id = "hawaii_aleutian_standard_time", nameRes = "Hawaii-Aleutian Standard Time", offsetSeconds = -36000),
"india_time_zone" to UnittoTimeZone(id = "india_time_zone", nameRes = "India Time Zone", offsetSeconds = 32400),
"indochina_time" to UnittoTimeZone(id = "indochina_time", nameRes = "Indochina Time", offsetSeconds = 25200),
"israel_daylight_time" to UnittoTimeZone(id = "israel_daylight_time", nameRes = "Israel Daylight Time", offsetSeconds = 10800),
"indian_chagos_time" to UnittoTimeZone(id = "indian_chagos_time", nameRes = "Indian Chagos Time", offsetSeconds = 21600),
"iran_daylight_time" to UnittoTimeZone(id = "iran_daylight_time", nameRes = "Iran Daylight Time", offsetSeconds = 16200),
"iran_summer_time" to UnittoTimeZone(id = "iran_summer_time", nameRes = "Iran Summer Time", offsetSeconds = 16200),
"iran_daylight_time" to UnittoTimeZone(id = "iran_daylight_time", nameRes = "Iran Daylight Time", offsetSeconds = 16200),
"irkutsk_summer_time" to UnittoTimeZone(id = "irkutsk_summer_time", nameRes = "Irkutsk Summer Time", offsetSeconds = 32400),
"irkutsk_time" to UnittoTimeZone(id = "irkutsk_time", nameRes = "Irkutsk Time", offsetSeconds = 28800),
"iran_standard_time" to UnittoTimeZone(id = "iran_standard_time", nameRes = "Iran Standard Time", offsetSeconds = 12600),
"iran_time" to UnittoTimeZone(id = "iran_time", nameRes = "Iran Time", offsetSeconds = 12600),
"india_standard_time" to UnittoTimeZone(id = "india_standard_time", nameRes = "India Standard Time", offsetSeconds = 19800),
"india_time" to UnittoTimeZone(id = "india_time", nameRes = "India Time", offsetSeconds = 19800),
"indian_standard_time" to UnittoTimeZone(id = "indian_standard_time", nameRes = "Indian Standard Time", offsetSeconds = 19800),
"irish_standard_time" to UnittoTimeZone(id = "irish_standard_time", nameRes = "Irish Standard Time", offsetSeconds = 3600),
"irish_summer_time" to UnittoTimeZone(id = "irish_summer_time", nameRes = "Irish Summer Time", offsetSeconds = 3600),
"israel_standard_time" to UnittoTimeZone(id = "israel_standard_time", nameRes = "Israel Standard Time", offsetSeconds = 7200),
"japan_standard_time" to UnittoTimeZone(id = "japan_standard_time", nameRes = "Japan Standard Time", offsetSeconds = 32400),
"kilo_time_zone" to UnittoTimeZone(id = "kilo_time_zone", nameRes = "Kilo Time Zone", offsetSeconds = 36000),
"kyrgyzstan_time" to UnittoTimeZone(id = "kyrgyzstan_time", nameRes = "Kyrgyzstan Time", offsetSeconds = 21600),
"kosrae_time" to UnittoTimeZone(id = "kosrae_time", nameRes = "Kosrae Time", offsetSeconds = 39600),
"krasnoyarsk_summer_time" to UnittoTimeZone(id = "krasnoyarsk_summer_time", nameRes = "Krasnoyarsk Summer Time", offsetSeconds = 28800),
"krasnoyarsk_time" to UnittoTimeZone(id = "krasnoyarsk_time", nameRes = "Krasnoyarsk Time", offsetSeconds = 25200),
"korea_standard_time" to UnittoTimeZone(id = "korea_standard_time", nameRes = "Korea Standard Time", offsetSeconds = 32400),
"korean_standard_time" to UnittoTimeZone(id = "korean_standard_time", nameRes = "Korean Standard Time", offsetSeconds = 32400),
"korea_time" to UnittoTimeZone(id = "korea_time", nameRes = "Korea Time", offsetSeconds = 32400),
"kuybyshev_time" to UnittoTimeZone(id = "kuybyshev_time", nameRes = "Kuybyshev Time", offsetSeconds = 14400),
"samara_summer_time" to UnittoTimeZone(id = "samara_summer_time", nameRes = "Samara Summer Time", offsetSeconds = 14400),
"lima_time_zone" to UnittoTimeZone(id = "lima_time_zone", nameRes = "Lima Time Zone", offsetSeconds = 39600),
"lord_howe_daylight_time" to UnittoTimeZone(id = "lord_howe_daylight_time", nameRes = "Lord Howe Daylight Time", offsetSeconds = 39600),
"lord_howe_standard_time" to UnittoTimeZone(id = "lord_howe_standard_time", nameRes = "Lord Howe Standard Time", offsetSeconds = 37800),
"line_islands_time" to UnittoTimeZone(id = "line_islands_time", nameRes = "Line Islands Time", offsetSeconds = 50400),
"mike_time_zone" to UnittoTimeZone(id = "mike_time_zone", nameRes = "Mike Time Zone", offsetSeconds = 43200),
"magadan_summer_time" to UnittoTimeZone(id = "magadan_summer_time", nameRes = "Magadan Summer Time", offsetSeconds = 43200),
"magadan_island_summer_time" to UnittoTimeZone(id = "magadan_island_summer_time", nameRes = "Magadan Island Summer Time", offsetSeconds = 43200),
"magadan_time" to UnittoTimeZone(id = "magadan_time", nameRes = "Magadan Time", offsetSeconds = 39600),
"magadan_island_time" to UnittoTimeZone(id = "magadan_island_time", nameRes = "Magadan Island Time", offsetSeconds = 39600),
"marquesas_time" to UnittoTimeZone(id = "marquesas_time", nameRes = "Marquesas Time", offsetSeconds = -34200),
"mawson_time" to UnittoTimeZone(id = "mawson_time", nameRes = "Mawson Time", offsetSeconds = 18000),
"mountain_daylight_time" to UnittoTimeZone(id = "mountain_daylight_time", nameRes = "Mountain Daylight Time", offsetSeconds = -21600),
"mountain_daylight_saving_time" to UnittoTimeZone(id = "mountain_daylight_saving_time", nameRes = "Mountain Daylight Saving Time", offsetSeconds = -21600),
"north_american_mountain_daylight_time" to UnittoTimeZone(id = "north_american_mountain_daylight_time", nameRes = "North American Mountain Daylight Time", offsetSeconds = -21600),
"heure_avanc_e_des_rocheuses_french" to UnittoTimeZone(id = "heure_avanc_e_des_rocheuses_french", nameRes = "Heure Avanc-e des Rocheuses (French)", offsetSeconds = -21600),
"marshall_islands_time" to UnittoTimeZone(id = "marshall_islands_time", nameRes = "Marshall Islands Time", offsetSeconds = 43200),
"myanmar_time" to UnittoTimeZone(id = "myanmar_time", nameRes = "Myanmar Time", offsetSeconds = 23400),
"moscow_daylight_time" to UnittoTimeZone(id = "moscow_daylight_time", nameRes = "Moscow Daylight Time", offsetSeconds = 14400),
"moscow_summer_time" to UnittoTimeZone(id = "moscow_summer_time", nameRes = "Moscow Summer Time", offsetSeconds = 14400),
"moscow_standard_time" to UnittoTimeZone(id = "moscow_standard_time", nameRes = "Moscow Standard Time", offsetSeconds = 10800),
"moscow_time" to UnittoTimeZone(id = "moscow_time", nameRes = "Moscow Time", offsetSeconds = 10800),
"mountain_standard_time" to UnittoTimeZone(id = "mountain_standard_time", nameRes = "Mountain Standard Time", offsetSeconds = -25200),
"mountain_time" to UnittoTimeZone(id = "mountain_time", nameRes = "Mountain Time", offsetSeconds = -25200),
"north_american_mountain_standard_time" to UnittoTimeZone(id = "north_american_mountain_standard_time", nameRes = "North American Mountain Standard Time", offsetSeconds = -25200),
"heure_normale_des_rocheuses_french" to UnittoTimeZone(id = "heure_normale_des_rocheuses_french", nameRes = "Heure Normale des Rocheuses (French)", offsetSeconds = -25200),
"mauritius_time" to UnittoTimeZone(id = "mauritius_time", nameRes = "Mauritius Time", offsetSeconds = 14400),
"maldives_time" to UnittoTimeZone(id = "maldives_time", nameRes = "Maldives Time", offsetSeconds = 18000),
"malaysia_time" to UnittoTimeZone(id = "malaysia_time", nameRes = "Malaysia Time", offsetSeconds = 28800),
"malaysian_standard_time" to UnittoTimeZone(id = "malaysian_standard_time", nameRes = "Malaysian Standard Time", offsetSeconds = 28800),
"november_time_zone" to UnittoTimeZone(id = "november_time_zone", nameRes = "November Time Zone", offsetSeconds = -3600),
"new_caledonia_time" to UnittoTimeZone(id = "new_caledonia_time", nameRes = "New Caledonia Time", offsetSeconds = 39600),
"newfoundland_daylight_time" to UnittoTimeZone(id = "newfoundland_daylight_time", nameRes = "Newfoundland Daylight Time", offsetSeconds = -9000),
"heure_avanc_e_de_terre_neuve_french" to UnittoTimeZone(id = "heure_avanc_e_de_terre_neuve_french", nameRes = "Heure Avanc-e de Terre-Neuve (French)", offsetSeconds = -9000),
"norfolk_daylight_time" to UnittoTimeZone(id = "norfolk_daylight_time", nameRes = "Norfolk Daylight Time", offsetSeconds = 43200),
"norfolk_island_daylight_time" to UnittoTimeZone(id = "norfolk_island_daylight_time", nameRes = "Norfolk Island Daylight Time", offsetSeconds = 43200),
"norfolk_time" to UnittoTimeZone(id = "norfolk_time", nameRes = "Norfolk Time", offsetSeconds = 39600),
"norfolk_island_time" to UnittoTimeZone(id = "norfolk_island_time", nameRes = "Norfolk Island Time", offsetSeconds = 39600),
"novosibirsk_summer_time" to UnittoTimeZone(id = "novosibirsk_summer_time", nameRes = "Novosibirsk Summer Time", offsetSeconds = 25200),
"omsk_summer_time" to UnittoTimeZone(id = "omsk_summer_time", nameRes = "Omsk Summer Time", offsetSeconds = 25200),
"novosibirsk_time" to UnittoTimeZone(id = "novosibirsk_time", nameRes = "Novosibirsk Time", offsetSeconds = 25200),
"omsk_standard_time" to UnittoTimeZone(id = "omsk_standard_time", nameRes = "Omsk Standard Time", offsetSeconds = 25200),
"nepal_time" to UnittoTimeZone(id = "nepal_time", nameRes = "Nepal Time", offsetSeconds = 20700),
"nauru_time" to UnittoTimeZone(id = "nauru_time", nameRes = "Nauru Time", offsetSeconds = 43200),
"newfoundland_standard_time" to UnittoTimeZone(id = "newfoundland_standard_time", nameRes = "Newfoundland Standard Time", offsetSeconds = -12600),
"heure_normale_de_terre_neuve_french" to UnittoTimeZone(id = "heure_normale_de_terre_neuve_french", nameRes = "Heure Normale de Terre-Neuve (French)", offsetSeconds = -12600),
"niue_time" to UnittoTimeZone(id = "niue_time", nameRes = "Niue Time", offsetSeconds = -39600),
"new_zealand_daylight_time" to UnittoTimeZone(id = "new_zealand_daylight_time", nameRes = "New Zealand Daylight Time", offsetSeconds = 46800),
"new_zealand_standard_time" to UnittoTimeZone(id = "new_zealand_standard_time", nameRes = "New Zealand Standard Time", offsetSeconds = 43200),
"oscar_time_zone" to UnittoTimeZone(id = "oscar_time_zone", nameRes = "Oscar Time Zone", offsetSeconds = -7200),
"omsk_summer_time" to UnittoTimeZone(id = "omsk_summer_time", nameRes = "Omsk Summer Time", offsetSeconds = 25200),
"novosibirsk_summer_time" to UnittoTimeZone(id = "novosibirsk_summer_time", nameRes = "Novosibirsk Summer Time", offsetSeconds = 25200),
"omsk_standard_time" to UnittoTimeZone(id = "omsk_standard_time", nameRes = "Omsk Standard Time", offsetSeconds = 21600),
"omsk_time" to UnittoTimeZone(id = "omsk_time", nameRes = "Omsk Time", offsetSeconds = 21600),
"novosibirsk_time" to UnittoTimeZone(id = "novosibirsk_time", nameRes = "Novosibirsk Time", offsetSeconds = 21600),
"oral_time" to UnittoTimeZone(id = "oral_time", nameRes = "Oral Time", offsetSeconds = 18000),
"papa_time_zone" to UnittoTimeZone(id = "papa_time_zone", nameRes = "Papa Time Zone", offsetSeconds = -10800),
"pacific_daylight_time" to UnittoTimeZone(id = "pacific_daylight_time", nameRes = "Pacific Daylight Time", offsetSeconds = -25200),
"pacific_daylight_saving_time" to UnittoTimeZone(id = "pacific_daylight_saving_time", nameRes = "Pacific Daylight Saving Time", offsetSeconds = -25200),
"north_american_pacific_daylight_time" to UnittoTimeZone(id = "north_american_pacific_daylight_time", nameRes = "North American Pacific Daylight Time", offsetSeconds = -25200),
"heure_avanc_e_du_pacifique_french" to UnittoTimeZone(id = "heure_avanc_e_du_pacifique_french", nameRes = "Heure Avanc-e du Pacifique (French)", offsetSeconds = -25200),
"peru_time" to UnittoTimeZone(id = "peru_time", nameRes = "Peru Time", offsetSeconds = -18000),
"kamchatka_summer_time" to UnittoTimeZone(id = "kamchatka_summer_time", nameRes = "Kamchatka Summer Time", offsetSeconds = 43200),
"kamchatka_time" to UnittoTimeZone(id = "kamchatka_time", nameRes = "Kamchatka Time", offsetSeconds = 43200),
"petropavlovsk_kamchatski_time" to UnittoTimeZone(id = "petropavlovsk_kamchatski_time", nameRes = "Petropavlovsk-Kamchatski Time", offsetSeconds = 43200),
"papua_new_guinea_time" to UnittoTimeZone(id = "papua_new_guinea_time", nameRes = "Papua New Guinea Time", offsetSeconds = 36000),
"phoenix_island_time" to UnittoTimeZone(id = "phoenix_island_time", nameRes = "Phoenix Island Time", offsetSeconds = 46800),
"philippine_time" to UnittoTimeZone(id = "philippine_time", nameRes = "Philippine Time", offsetSeconds = 28800),
"philippine_standard_time" to UnittoTimeZone(id = "philippine_standard_time", nameRes = "Philippine Standard Time", offsetSeconds = 28800),
"pakistan_standard_time" to UnittoTimeZone(id = "pakistan_standard_time", nameRes = "Pakistan Standard Time", offsetSeconds = 18000),
"pakistan_time" to UnittoTimeZone(id = "pakistan_time", nameRes = "Pakistan Time", offsetSeconds = 18000),
"pierre_&_miquelon_daylight_time" to UnittoTimeZone(id = "pierre_&_miquelon_daylight_time", nameRes = "Pierre & Miquelon Daylight Time", offsetSeconds = -7200),
"pierre_&_miquelon_standard_time" to UnittoTimeZone(id = "pierre_&_miquelon_standard_time", nameRes = "Pierre & Miquelon Standard Time", offsetSeconds = -10800),
"pohnpei_standard_time" to UnittoTimeZone(id = "pohnpei_standard_time", nameRes = "Pohnpei Standard Time", offsetSeconds = 39600),
"pacific_standard_time" to UnittoTimeZone(id = "pacific_standard_time", nameRes = "Pacific Standard Time", offsetSeconds = -28800),
"pacific_time" to UnittoTimeZone(id = "pacific_time", nameRes = "Pacific Time", offsetSeconds = -28800),
"north_american_pacific_standard_time" to UnittoTimeZone(id = "north_american_pacific_standard_time", nameRes = "North American Pacific Standard Time", offsetSeconds = -28800),
"tiempo_del_pac_fico_spanish" to UnittoTimeZone(id = "tiempo_del_pac_fico_spanish", nameRes = "Tiempo del Pac-fico (Spanish)", offsetSeconds = -28800),
"heure_normale_du_pacifique_french" to UnittoTimeZone(id = "heure_normale_du_pacifique_french", nameRes = "Heure Normale du Pacifique (French)", offsetSeconds = -28800),
"pitcairn_standard_time" to UnittoTimeZone(id = "pitcairn_standard_time", nameRes = "Pitcairn Standard Time", offsetSeconds = -28800),
"palau_time" to UnittoTimeZone(id = "palau_time", nameRes = "Palau Time", offsetSeconds = 32400),
"paraguay_summer_time" to UnittoTimeZone(id = "paraguay_summer_time", nameRes = "Paraguay Summer Time", offsetSeconds = -10800),
"paraguay_time" to UnittoTimeZone(id = "paraguay_time", nameRes = "Paraguay Time", offsetSeconds = -14400),
"pyongyang_time" to UnittoTimeZone(id = "pyongyang_time", nameRes = "Pyongyang Time", offsetSeconds = 30600),
"pyongyang_standard_time" to UnittoTimeZone(id = "pyongyang_standard_time", nameRes = "Pyongyang Standard Time", offsetSeconds = 30600),
"quebec_time_zone" to UnittoTimeZone(id = "quebec_time_zone", nameRes = "Quebec Time Zone", offsetSeconds = -14400),
"qyzylorda_time" to UnittoTimeZone(id = "qyzylorda_time", nameRes = "Qyzylorda Time", offsetSeconds = 21600),
"romeo_time_zone" to UnittoTimeZone(id = "romeo_time_zone", nameRes = "Romeo Time Zone", offsetSeconds = -18000),
"reunion_time" to UnittoTimeZone(id = "reunion_time", nameRes = "Reunion Time", offsetSeconds = 14400),
"rothera_time" to UnittoTimeZone(id = "rothera_time", nameRes = "Rothera Time", offsetSeconds = -10800),
"sierra_time_zone" to UnittoTimeZone(id = "sierra_time_zone", nameRes = "Sierra Time Zone", offsetSeconds = -21600),
"sakhalin_time" to UnittoTimeZone(id = "sakhalin_time", nameRes = "Sakhalin Time", offsetSeconds = 39600),
"samara_time" to UnittoTimeZone(id = "samara_time", nameRes = "Samara Time", offsetSeconds = 14400),
"samara_standard_time" to UnittoTimeZone(id = "samara_standard_time", nameRes = "Samara Standard Time", offsetSeconds = 14400),
"south_africa_standard_time" to UnittoTimeZone(id = "south_africa_standard_time", nameRes = "South Africa Standard Time", offsetSeconds = 7200),
"south_african_standard_time" to UnittoTimeZone(id = "south_african_standard_time", nameRes = "South African Standard Time", offsetSeconds = 7200),
"solomon_islands_time" to UnittoTimeZone(id = "solomon_islands_time", nameRes = "Solomon Islands Time", offsetSeconds = 39600),
"solomon_island_time" to UnittoTimeZone(id = "solomon_island_time", nameRes = "Solomon Island Time", offsetSeconds = 39600),
"seychelles_time" to UnittoTimeZone(id = "seychelles_time", nameRes = "Seychelles Time", offsetSeconds = 14400),
"singapore_time" to UnittoTimeZone(id = "singapore_time", nameRes = "Singapore Time", offsetSeconds = 28800),
"singapore_standard_time" to UnittoTimeZone(id = "singapore_standard_time", nameRes = "Singapore Standard Time", offsetSeconds = 28800),
"srednekolymsk_time" to UnittoTimeZone(id = "srednekolymsk_time", nameRes = "Srednekolymsk Time", offsetSeconds = 39600),
"suriname_time" to UnittoTimeZone(id = "suriname_time", nameRes = "Suriname Time", offsetSeconds = -10800),
"samoa_standard_time" to UnittoTimeZone(id = "samoa_standard_time", nameRes = "Samoa Standard Time", offsetSeconds = -39600),
"syowa_time" to UnittoTimeZone(id = "syowa_time", nameRes = "Syowa Time", offsetSeconds = 10800),
"tango_time_zone" to UnittoTimeZone(id = "tango_time_zone", nameRes = "Tango Time Zone", offsetSeconds = -25200),
"tahiti_time" to UnittoTimeZone(id = "tahiti_time", nameRes = "Tahiti Time", offsetSeconds = -36000),
"french_southern_and_antarctic_time" to UnittoTimeZone(id = "french_southern_and_antarctic_time", nameRes = "French Southern and Antarctic Time", offsetSeconds = 18000),
"kerguelen_islands_time" to UnittoTimeZone(id = "kerguelen_islands_time", nameRes = "Kerguelen (Islands) Time", offsetSeconds = 18000),
"tajikistan_time" to UnittoTimeZone(id = "tajikistan_time", nameRes = "Tajikistan Time", offsetSeconds = 18000),
"tokelau_time" to UnittoTimeZone(id = "tokelau_time", nameRes = "Tokelau Time", offsetSeconds = 46800),
"east_timor_time" to UnittoTimeZone(id = "east_timor_time", nameRes = "East Timor Time", offsetSeconds = 32400),
"turkmenistan_time" to UnittoTimeZone(id = "turkmenistan_time", nameRes = "Turkmenistan Time", offsetSeconds = 18000),
"tonga_summer_time" to UnittoTimeZone(id = "tonga_summer_time", nameRes = "Tonga Summer Time", offsetSeconds = 50400),
"tonga_time" to UnittoTimeZone(id = "tonga_time", nameRes = "Tonga Time", offsetSeconds = 46800),
"turkey_time" to UnittoTimeZone(id = "turkey_time", nameRes = "Turkey Time", offsetSeconds = 10800),
"tuvalu_time" to UnittoTimeZone(id = "tuvalu_time", nameRes = "Tuvalu Time", offsetSeconds = 43200),
"uniform_time_zone" to UnittoTimeZone(id = "uniform_time_zone", nameRes = "Uniform Time Zone", offsetSeconds = -28800),
"ulaanbaatar_summer_time" to UnittoTimeZone(id = "ulaanbaatar_summer_time", nameRes = "Ulaanbaatar Summer Time", offsetSeconds = 32400),
"ulan_bator_summer_time" to UnittoTimeZone(id = "ulan_bator_summer_time", nameRes = "Ulan Bator Summer Time", offsetSeconds = 32400),
"ulaanbaatar_time" to UnittoTimeZone(id = "ulaanbaatar_time", nameRes = "Ulaanbaatar Time", offsetSeconds = 28800),
"ulan_bator_time" to UnittoTimeZone(id = "ulan_bator_time", nameRes = "Ulan Bator Time", offsetSeconds = 28800),
"coordinated_universal_time" to UnittoTimeZone(id = "coordinated_universal_time", nameRes = "Coordinated Universal Time", offsetSeconds = 0),
"uruguay_summer_time" to UnittoTimeZone(id = "uruguay_summer_time", nameRes = "Uruguay Summer Time", offsetSeconds = -7200),
"uruguay_time" to UnittoTimeZone(id = "uruguay_time", nameRes = "Uruguay Time", offsetSeconds = -10800),
"uzbekistan_time" to UnittoTimeZone(id = "uzbekistan_time", nameRes = "Uzbekistan Time", offsetSeconds = 18000),
"victor_time_zone" to UnittoTimeZone(id = "victor_time_zone", nameRes = "Victor Time Zone", offsetSeconds = -32400),
"venezuelan_standard_time" to UnittoTimeZone(id = "venezuelan_standard_time", nameRes = "Venezuelan Standard Time", offsetSeconds = -14400),
"hora_legal_de_venezuela_spanish" to UnittoTimeZone(id = "hora_legal_de_venezuela_spanish", nameRes = "Hora Legal de Venezuela (Spanish)", offsetSeconds = -14400),
"vladivostok_summer_time" to UnittoTimeZone(id = "vladivostok_summer_time", nameRes = "Vladivostok Summer Time", offsetSeconds = 39600),
"vladivostok_time" to UnittoTimeZone(id = "vladivostok_time", nameRes = "Vladivostok Time", offsetSeconds = 36000),
"vostok_time" to UnittoTimeZone(id = "vostok_time", nameRes = "Vostok Time", offsetSeconds = 21600),
"vanuatu_time" to UnittoTimeZone(id = "vanuatu_time", nameRes = "Vanuatu Time", offsetSeconds = 39600),
"efate_time" to UnittoTimeZone(id = "efate_time", nameRes = "Efate Time", offsetSeconds = 39600),
"whiskey_time_zone" to UnittoTimeZone(id = "whiskey_time_zone", nameRes = "Whiskey Time Zone", offsetSeconds = -36000),
"wake_time" to UnittoTimeZone(id = "wake_time", nameRes = "Wake Time", offsetSeconds = 43200),
"western_argentine_summer_time" to UnittoTimeZone(id = "western_argentine_summer_time", nameRes = "Western Argentine Summer Time", offsetSeconds = -10800),
"west_africa_summer_time" to UnittoTimeZone(id = "west_africa_summer_time", nameRes = "West Africa Summer Time", offsetSeconds = 7200),
"west_africa_time" to UnittoTimeZone(id = "west_africa_time", nameRes = "West Africa Time", offsetSeconds = 3600),
"western_european_summer_time" to UnittoTimeZone(id = "western_european_summer_time", nameRes = "Western European Summer Time", offsetSeconds = 3600),
"western_european_daylight_time" to UnittoTimeZone(id = "western_european_daylight_time", nameRes = "Western European Daylight Time", offsetSeconds = 3600),
"westeurop_ische_sommerzeit_german" to UnittoTimeZone(id = "westeurop_ische_sommerzeit_german", nameRes = "Westeurop-ische Sommerzeit (German)", offsetSeconds = 3600),
"western_european_time" to UnittoTimeZone(id = "western_european_time", nameRes = "Western European Time", offsetSeconds = 0),
"greenwich_mean_time" to UnittoTimeZone(id = "greenwich_mean_time", nameRes = "Greenwich Mean Time", offsetSeconds = 0),
"westeurop_ische_zeit_german" to UnittoTimeZone(id = "westeurop_ische_zeit_german", nameRes = "Westeurop-ische Zeit (German)", offsetSeconds = 0),
"wallis_and_futuna_time" to UnittoTimeZone(id = "wallis_and_futuna_time", nameRes = "Wallis and Futuna Time", offsetSeconds = 43200),
"western_greenland_summer_time" to UnittoTimeZone(id = "western_greenland_summer_time", nameRes = "Western Greenland Summer Time", offsetSeconds = -7200),
"west_greenland_summer_time" to UnittoTimeZone(id = "west_greenland_summer_time", nameRes = "West Greenland Summer Time", offsetSeconds = -7200),
"west_greenland_time" to UnittoTimeZone(id = "west_greenland_time", nameRes = "West Greenland Time", offsetSeconds = -10800),
"western_greenland_time" to UnittoTimeZone(id = "western_greenland_time", nameRes = "Western Greenland Time", offsetSeconds = -10800),
"western_indonesian_time" to UnittoTimeZone(id = "western_indonesian_time", nameRes = "Western Indonesian Time", offsetSeconds = 25200),
"waktu_indonesia_barat" to UnittoTimeZone(id = "waktu_indonesia_barat", nameRes = "Waktu Indonesia Barat", offsetSeconds = 25200),
"eastern_indonesian_time" to UnittoTimeZone(id = "eastern_indonesian_time", nameRes = "Eastern Indonesian Time", offsetSeconds = 32400),
"waktu_indonesia_timur" to UnittoTimeZone(id = "waktu_indonesia_timur", nameRes = "Waktu Indonesia Timur", offsetSeconds = 32400),
"central_indonesian_time" to UnittoTimeZone(id = "central_indonesian_time", nameRes = "Central Indonesian Time", offsetSeconds = 28800),
"waktu_indonesia_tengah" to UnittoTimeZone(id = "waktu_indonesia_tengah", nameRes = "Waktu Indonesia Tengah", offsetSeconds = 28800),
"west_samoa_time" to UnittoTimeZone(id = "west_samoa_time", nameRes = "West Samoa Time", offsetSeconds = 46800),
"samoa_time" to UnittoTimeZone(id = "samoa_time", nameRes = "Samoa Time", offsetSeconds = 46800),
"western_sahara_summer_time" to UnittoTimeZone(id = "western_sahara_summer_time", nameRes = "Western Sahara Summer Time", offsetSeconds = 3600),
"western_sahara_standard_time" to UnittoTimeZone(id = "western_sahara_standard_time", nameRes = "Western Sahara Standard Time", offsetSeconds = 0),
"x_ray_time_zone" to UnittoTimeZone(id = "x_ray_time_zone", nameRes = "X-ray Time Zone", offsetSeconds = -39600),
"yankee_time_zone" to UnittoTimeZone(id = "yankee_time_zone", nameRes = "Yankee Time Zone", offsetSeconds = -43200),
"yakutsk_summer_time" to UnittoTimeZone(id = "yakutsk_summer_time", nameRes = "Yakutsk Summer Time", offsetSeconds = 36000),
"yakutsk_time" to UnittoTimeZone(id = "yakutsk_time", nameRes = "Yakutsk Time", offsetSeconds = 32400),
"yap_time" to UnittoTimeZone(id = "yap_time", nameRes = "Yap Time", offsetSeconds = 36000),
"yekaterinburg_summer_time" to UnittoTimeZone(id = "yekaterinburg_summer_time", nameRes = "Yekaterinburg Summer Time", offsetSeconds = 21600),
"yekaterinburg_time" to UnittoTimeZone(id = "yekaterinburg_time", nameRes = "Yekaterinburg Time", offsetSeconds = 18000),
"zulu_time_zone" to UnittoTimeZone(id = "zulu_time_zone", nameRes = "Zulu Time Zone", offsetSeconds = 0)
)
val favoriteTimeZones: MutableStateFlow<List<UnittoTimeZone>> = MutableStateFlow(emptyList())
// UNCOMMENT FOR RELEASE
// val favoriteTimeZones: Flow<List<UnittoTimeZone>> = dao
// .getAll()
// .map { list ->
// val favorites = mutableListOf<UnittoTimeZone>()
// list.forEach { entity ->
// val foundTimeZone = allTimeZones[entity.id] ?: return@forEach
// val mapped = foundTimeZone.copy(
// position = entity.position
// )
// favorites.add(mapped)
// }
//
// favorites
// }
suspend fun swapTimeZones(from: String, to: String) = withContext(Dispatchers.IO) {
// UNCOMMENT FOR RELEASE
// dao.swap(from, to)
favoriteTimeZones.update {
val fromIndex = it.indexOfFirst { it.id == from }
val toIndex = it.indexOfFirst { it.id == to }
it
.toMutableList()
.apply {
add(toIndex, removeAt(fromIndex))
}
}
return@withContext
}
suspend fun delete(timeZone: UnittoTimeZone) = withContext(Dispatchers.IO) {
// UNCOMMENT FOR RELEASE
// // Only PrimaryKey is needed
// dao.remove(TimeZoneEntity(id = timeZone.id, position = 0))
favoriteTimeZones.update { it.minus(timeZone) }
}
suspend fun filterAllTimeZones(searchQuery: String): List<UnittoTimeZone> =
withContext(Dispatchers.IO) {
val query = searchQuery.trim().lowercase()
val threshold: Int = query.length / 2
val timeZonesWithDist = mutableListOf<Pair<UnittoTimeZone, Int>>()
allTimeZones.values.forEach { timeZone ->
val timeZoneName = timeZone.nameRes
if (timeZone.code.lowercase() == query) {
timeZonesWithDist.add(timeZone to 1)
return@forEach
}
when {
// not zero, so that lev can have that
timeZoneName.startsWith(query) -> {
timeZonesWithDist.add(timeZone to 1)
return@forEach
}
timeZoneName.contains(query) -> {
timeZonesWithDist.add(timeZone to 2)
return@forEach
}
}
val levDist = timeZoneName
.substring(0, minOf(query.length, timeZoneName.length))
.lev(query)
if (levDist < threshold) {
timeZonesWithDist.add(timeZone to levDist)
}
}
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()
// )
// )
favoriteTimeZones.update { it.plus(timeZone) }
}
}

View File

@ -92,6 +92,7 @@ data class UIPreferences(
val customColor: Color = Color.Unspecified,
val monetMode: MonetMode = MonetMode.TONAL_SPOT,
val startingScreen: String = TopLevelDestinations.Converter.route,
val enableToolsExperiment: Boolean = false,
)
data class MainPreferences(
@ -153,6 +154,7 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
val monetMode: MonetMode = preferences[PrefsKeys.MONET_MODE]?.let { MonetMode.valueOf(it) }
?: MonetMode.TONAL_SPOT
val startingScreen: String = preferences[PrefsKeys.STARTING_SCREEN] ?: TopLevelDestinations.Converter.route
val enableToolsExperiment: Boolean = preferences[PrefsKeys.ENABLE_TOOLS_EXPERIMENT] ?: false
UIPreferences(
themingMode = themingMode,
@ -160,7 +162,8 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
enableAmoledTheme = enableAmoledTheme,
customColor = customColor,
monetMode = monetMode,
startingScreen = startingScreen
startingScreen = startingScreen,
enableToolsExperiment = enableToolsExperiment
)
}
@ -228,7 +231,7 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
latestRightSideUnit = main.latestRightSideUnit,
shownUnitGroups = main.shownUnitGroups,
enableVibrations = main.enableVibrations,
enableToolsExperiment = false,
enableToolsExperiment = ui.enableToolsExperiment,
startingScreen = ui.startingScreen,
radianMode = main.radianMode,
unitConverterFavoritesOnly = main.unitConverterFavoritesOnly,

View File

@ -18,7 +18,6 @@
package com.sadellie.unitto.feature.datedifference
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
@ -34,7 +33,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -62,8 +60,10 @@ internal fun DateDifferenceRoute(
navigateToMenu = navigateToMenu,
navigateToSettings = navigateToSettings,
uiState = uiState.value,
updateStart = viewModel::setStartTime,
updateEnd = viewModel::setEndTime
setStartTime = viewModel::setStartTime,
setEndTime = viewModel::setEndTime,
setStartDate = viewModel::setStartDate,
setEndDate = viewModel::setEndDate,
)
}
@ -72,12 +72,13 @@ internal fun DateDifferenceRoute(
internal fun DateDifferenceScreen(
navigateToMenu: () -> Unit,
navigateToSettings: () -> Unit,
updateStart: (LocalDateTime) -> Unit,
updateEnd: (LocalDateTime) -> Unit,
setStartTime: (Int, Int) -> Unit,
setEndTime: (Int, Int) -> Unit,
setStartDate: (LocalDateTime) -> Unit,
setEndDate: (LocalDateTime) -> Unit,
uiState: UIState,
) {
var dialogState by remember { mutableStateOf(DialogState.NONE) }
val isVertical = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
UnittoScreenWithTopBar(
title = { Text(stringResource(R.string.date_difference)) },
@ -138,26 +139,26 @@ internal fun DateDifferenceScreen(
when (dialogState) {
DialogState.FROM -> {
TimePickerDialog(
localDateTime = uiState.start,
hour = uiState.start.hour,
minute = uiState.start.minute,
onDismiss = ::resetDialog,
onConfirm = {
updateStart(it)
onConfirm = { hour, minute ->
setStartTime(hour, minute)
dialogState = DialogState.FROM_DATE
},
confirmLabel = stringResource(R.string.next_label),
vertical = isVertical,
)
}
DialogState.FROM_TIME -> {
TimePickerDialog(
localDateTime = uiState.start,
hour = uiState.start.hour,
minute = uiState.start.minute,
onDismiss = ::resetDialog,
onConfirm = {
updateStart(it)
onConfirm = { hour, minute ->
setStartTime(hour, minute)
resetDialog()
},
vertical = isVertical,
)
}
@ -166,7 +167,7 @@ internal fun DateDifferenceScreen(
localDateTime = uiState.start,
onDismiss = ::resetDialog,
onConfirm = {
updateStart(it)
setStartDate(it)
resetDialog()
}
)
@ -174,26 +175,26 @@ internal fun DateDifferenceScreen(
DialogState.TO -> {
TimePickerDialog(
localDateTime = uiState.end,
hour = uiState.end.hour,
minute = uiState.end.minute,
onDismiss = ::resetDialog,
onConfirm = {
updateEnd(it)
onConfirm = { hour, minute ->
setEndTime(hour, minute)
dialogState = DialogState.TO_DATE
},
confirmLabel = stringResource(R.string.next_label),
vertical = isVertical,
)
}
DialogState.TO_TIME -> {
TimePickerDialog(
localDateTime = uiState.end,
hour = uiState.end.hour,
minute = uiState.end.minute,
onDismiss = ::resetDialog,
onConfirm = {
updateEnd(it)
onConfirm = { hour, minute ->
setEndTime(hour, minute)
resetDialog()
},
vertical = isVertical,
)
}
@ -202,7 +203,7 @@ internal fun DateDifferenceScreen(
localDateTime = uiState.end,
onDismiss = ::resetDialog,
onConfirm = {
updateEnd(it)
setEndDate(it)
resetDialog()
}
)
@ -222,10 +223,12 @@ private fun DateDifferenceScreenPreview() {
DateDifferenceScreen(
navigateToMenu = {},
navigateToSettings = {},
updateStart = {},
updateEnd = {},
setStartTime = { _, _ -> },
setEndTime = { _, _ -> },
uiState = UIState(
result = DateDifference.Default(4, 1, 2, 3, 4)
)
),
setStartDate = {},
setEndDate = {},
)
}

View File

@ -46,9 +46,13 @@ internal class DateDifferenceViewModel @Inject constructor() : ViewModel() {
viewModelScope, SharingStarted.WhileSubscribed(5000L), UIState()
)
fun setStartTime(newTime: LocalDateTime) = _start.update { newTime }
fun setStartTime(hour: Int, minute: Int) = _start.update { it.withHour(hour).withMinute(minute) }
fun setEndTime(newTime: LocalDateTime) = _end.update { newTime }
fun setEndTime(hour: Int, minute: Int) = _end.update { it.withHour(hour).withMinute(minute) }
fun setStartDate(dateTime: LocalDateTime) = _start.update { dateTime }
fun setEndDate(dateTime: LocalDateTime) = _end.update { dateTime }
init {
viewModelScope.launch(Dispatchers.Default) {

View File

@ -30,9 +30,11 @@ android {
dependencies {
testImplementation(libs.junit)
implementation(libs.com.github.sadellie.themmo)
implementation(libs.org.burnoutcrew.composereorderable)
implementation(project(mapOf("path" to ":data:common")))
implementation(project(mapOf("path" to ":data:userprefs")))
implementation(project(mapOf("path" to ":data:database")))
implementation(project(mapOf("path" to ":data:timezone")))
implementation(project(mapOf("path" to ":data:model")))
}

View File

@ -0,0 +1,156 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.timezone
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
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.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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.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.data.model.UnittoTimeZone
import com.sadellie.unitto.timezone.components.SelectableTimeZone
import com.sadellie.unitto.timezone.components.UnittoSearchBar
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import java.time.ZonedDateTime
@Composable
internal fun AddTimeZoneRoute(
viewModel: AddTimeZoneViewModel = hiltViewModel(),
navigateUp: () -> Unit,
userTime: ZonedDateTime? = null
) {
val uiState = viewModel.addTimeZoneUIState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
if (userTime == null) {
while (isActive) {
viewModel.setTime(ZonedDateTime.now())
delay(1000)
}
} else {
viewModel.setTime(userTime)
}
}
AddTimeZoneScreen(
uiState = uiState.value,
navigateUp = navigateUp,
onQueryChange = viewModel::onQueryChange,
addToFavorites = viewModel::addToFavorites,
)
}
@Composable
fun AddTimeZoneScreen(
uiState: AddTimeZoneUIState,
navigateUp: () -> Unit,
onQueryChange: (String) -> Unit,
addToFavorites: (UnittoTimeZone) -> Unit,
) {
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"
)
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
UnittoSearchBar(
modifier = Modifier,
query = uiState.query,
onQueryChange = onQueryChange,
navigateUp = navigateUp,
title = stringResource(R.string.add_time_zone_title),
placeholder = stringResource(R.string.search_text_field_placeholder),
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = searchBarBackground.value
)
)
},
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues),
state = listState
) {
items(uiState.list) {
SelectableTimeZone(
timeZone = it,
modifier = Modifier
.clickable { addToFavorites(it); navigateUp() }
.fillMaxWidth(),
currentTime = uiState.userTime
)
Divider()
}
}
}
}
@Preview
@Composable
fun PreviewAddTimeZoneScreen() {
AddTimeZoneScreen(
navigateUp = {},
uiState = AddTimeZoneUIState(
list = List(50) {
UnittoTimeZone(
id = "timezone $it",
nameRes = "Time zone $it",
)
}
),
onQueryChange = {},
addToFavorites = {},
)
}

View File

@ -0,0 +1,28 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.timezone
import com.sadellie.unitto.data.model.UnittoTimeZone
import java.time.ZonedDateTime
data class AddTimeZoneUIState(
val query: String = "",
val list: List<UnittoTimeZone> = emptyList(),
val userTime: ZonedDateTime? = null,
)

View File

@ -0,0 +1,82 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.timezone
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.model.UnittoTimeZone
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
@HiltViewModel
class AddTimeZoneViewModel @Inject constructor(
private val timezonesRepository: TimeZonesRepository,
) : ViewModel() {
private val _userTime = MutableStateFlow<ZonedDateTime?>(ZonedDateTime.now())
private val _query = MutableStateFlow("")
private val _filteredTimeZones = MutableStateFlow(emptyList<UnittoTimeZone>())
val addTimeZoneUIState = combine(
_query,
_filteredTimeZones,
_userTime,
) { query, filteredTimeZone, userTime ->
return@combine AddTimeZoneUIState(
query = query,
list = filteredTimeZone,
userTime = userTime,
)
}.stateIn(
viewModelScope, SharingStarted.WhileSubscribed(5000), AddTimeZoneUIState()
)
fun onQueryChange(query: String) {
_query.update { query }
filterTimeZones(query)
}
private fun filterTimeZones(query: String = "") = viewModelScope.launch {
_filteredTimeZones.update {
timezonesRepository.filterAllTimeZones(query)
}
}
fun addToFavorites(timeZone: UnittoTimeZone) = viewModelScope.launch(Dispatchers.IO) {
timezonesRepository.addToFavorites(timeZone)
}
fun setTime(time: ZonedDateTime) = viewModelScope.launch(Dispatchers.Default) {
_userTime.update { time }
}
init {
// TODO Maybe only when actually needed?
filterTimeZones()
}
}

View File

@ -18,10 +18,364 @@
package com.sadellie.unitto.timezone
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.tween
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.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.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.outlined.History
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.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
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.res.stringResource
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.MenuButton
import com.sadellie.unitto.core.ui.common.SettingsButton
import com.sadellie.unitto.core.ui.common.TimePickerDialog
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
import com.sadellie.unitto.core.ui.common.squashable
import com.sadellie.unitto.core.ui.datetime.formatDayMonthYear
import com.sadellie.unitto.core.ui.datetime.formatLocal
import com.sadellie.unitto.core.ui.datetime.formatTimeZoneOffset
import com.sadellie.unitto.core.ui.theme.AppTypography
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.timezone.components.TimeZoneListItem
import io.github.sadellie.themmo.Themmo
import io.github.sadellie.themmo.rememberThemmoController
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
@Composable
internal fun TimeZoneRoute() {}
internal fun TimeZoneRoute(
viewModel: TimeZoneViewModel = hiltViewModel(),
navigateToMenu: () -> Unit,
navigateToSettings: () -> 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
)
},
onDragEnd = viewModel::onDragEnd,
onDelete = viewModel::onDelete,
setSelectedTime = viewModel::setCustomTime,
setCurrentTime = viewModel::setCurrentTime,
resetTime = viewModel::resetTime,
)
}
@Composable
private fun TimeZoneScreen() {}
private fun TimeZoneScreen(
uiState: TimeZoneUIState,
navigateToMenu: () -> Unit,
navigateToSettings: () -> Unit,
navigateToAddTimeZone: () -> Unit,
onDragEnd: (String, String) -> Unit,
onDelete: (UnittoTimeZone) -> Unit,
setSelectedTime: (ZonedDateTime) -> Unit,
setCurrentTime: () -> Unit,
resetTime: () -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
LaunchedEffect(uiState.updateTime) {
while (uiState.updateTime and isActive) {
setCurrentTime()
delay(1000)
}
}
val copiedList = rememberUpdatedState(newValue = uiState.list) as MutableState
val state = rememberReorderableLazyListState(
onMove = 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
}
)
// TODO Unswipe on dragging
var swiped by remember<MutableState<UnittoTimeZone?>> { 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_screen)) },
navigationIcon = { MenuButton(navigateToMenu) },
actions = { SettingsButton(navigateToSettings) },
floatingActionButton = {
LargeFloatingActionButton(navigateToAddTimeZone) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = null,
modifier = Modifier.size(FloatingActionButtonDefaults.LargeIconSize),
)
}
},
floatingActionButtonPosition = FabPosition.Center,
scrollBehavior = scrollBehavior,
) { paddingValues ->
LazyColumn(
state = state.listState,
modifier = Modifier
.padding(paddingValues)
.fillMaxHeight()
.reorderable(state)
.detectReorderAfterLongPress(state),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(bottom = 124.dp)
) {
// This is a fake item. First item in list can not animated, so we do this magic fuckery
item {
UserTimeZone(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
userTime = uiState.userTime,
onClick = { showTimeSelector = true },
onResetClick = resetTime,
showReset = !uiState.updateTime,
)
}
items(copiedList.value, { it.id }) { item ->
ReorderableItem(state, key = item.id) { isDragging ->
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
}
val scope = rememberCoroutineScope()
val draggableState = rememberDraggableTimeZone(maxDrag) { swiped = item }
LaunchedEffect(swiped) {
if (swiped != item) scope.launch {
draggableState.animateTo(false)
}
}
TimeZoneListItem(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = itemPadding),
timeZone = item,
currentTime = uiState.userTime,
onDelete = onDelete,
color = background,
onSwipe = {},
draggableState = draggableState,
)
}
}
}
}
if (showTimeSelector) {
TimePickerDialog(
hour = uiState.userTime.hour,
minute = uiState.userTime.minute,
onConfirm = { hour, minute ->
setSelectedTime(
uiState.userTime
.withHour(hour)
.withMinute(minute)
)
showTimeSelector = false
},
onDismiss = { showTimeSelector = false }
)
}
}
@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.formatTimeZoneOffset(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
// TODO Swipe to increase, touch to set
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.formatDayMonthYear(),
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
}
)
}
@Preview
@Composable
fun PreviewTimeZoneScreen() {
Themmo(
themmoController = rememberThemmoController(
lightColorScheme = LightThemeColors,
darkColorScheme = DarkThemeColors,
),
typography = AppTypography,
) {
TimeZoneScreen(
uiState = TimeZoneUIState(
list = List(50) {
UnittoTimeZone(
id = "timezone $it",
nameRes = "Time zone $it",
)
}
),
navigateToMenu = {},
navigateToSettings = {},
navigateToAddTimeZone = {},
onDragEnd = { _, _ -> },
onDelete = {},
setSelectedTime = {},
setCurrentTime = {},
resetTime = {},
)
}
}

View File

@ -0,0 +1,28 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.timezone
import com.sadellie.unitto.data.model.UnittoTimeZone
import java.time.ZonedDateTime
data class TimeZoneUIState(
val list: List<UnittoTimeZone> = emptyList(),
val userTime: ZonedDateTime = ZonedDateTime.now(),
val updateTime: Boolean = true,
)

View File

@ -0,0 +1,78 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.timezone
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.model.UnittoTimeZone
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
@HiltViewModel
class TimeZoneViewModel @Inject constructor(
private val timezonesRepository: TimeZonesRepository
): ViewModel() {
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
)
}.stateIn(
viewModelScope, SharingStarted.WhileSubscribed(5000), TimeZoneUIState()
)
fun onDragEnd(from: String, to: String) = viewModelScope.launch {
timezonesRepository.swapTimeZones(from, to)
}
fun onDelete(timeZone: UnittoTimeZone) = viewModelScope.launch {
timezonesRepository.delete(timeZone)
}
fun setCustomTime(time: ZonedDateTime) = viewModelScope.launch(Dispatchers.Default) {
_updateTime.update { false }
_userTime.update { time }
}
fun resetTime() = viewModelScope.launch(Dispatchers.Default) {
_updateTime.update { true }
}
fun setCurrentTime() = viewModelScope.launch(Dispatchers.Default) {
_userTime.update { ZonedDateTime.now() }
}
}

View File

@ -0,0 +1,71 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.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(),
)
}

View File

@ -0,0 +1,186 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.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<Boolean>,
) {
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
)
}

View File

@ -0,0 +1,193 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.timezone.components
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.NavigateUpButton
@Composable
fun UnittoSearchBar(
modifier: Modifier = Modifier,
query: String,
onQueryChange: (String) -> Unit,
navigateUp: () -> Unit,
title: String,
searchActions: @Composable (RowScope.() -> Unit) = {},
noSearchActions: @Composable (RowScope.() -> Unit) = {},
placeholder: String,
scrollBehavior: TopAppBarScrollBehavior? = null,
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors()
) {
var showSearch by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
fun stagedNavigateUp() {
if (showSearch) {
// Search text field is open, need to close it and clear search query
showSearch = false
// focusManager.clearFocus()
onQueryChange("")
} else {
// No search text field is shown, can go back as usual
navigateUp()
}
}
TopAppBar(
modifier = modifier,
title = {
Crossfade(showSearch) { _showSearch ->
if (_showSearch) {
LaunchedEffect(Unit) { focusRequester.requestFocus() }
SearchTextField(
modifier = Modifier
.focusRequester(focusRequester),
value = query,
placeholder = placeholder,
onValueChange = onQueryChange,
onSearch = {}
)
} else {
Text(
text = title,
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.titleLarge
)
}
}
},
navigationIcon = {
NavigateUpButton { stagedNavigateUp() }
},
actions = {
Crossfade(showSearch) { _showSearch ->
if (_showSearch) {
ClearButton(visible = query.isNotEmpty()) { onQueryChange("") }
searchActions()
} else {
SearchButton { showSearch = true }
noSearchActions()
}
}
},
scrollBehavior = scrollBehavior,
colors = colors,
)
BackHandler { stagedNavigateUp() }
}
@Composable
private fun SearchTextField(
modifier: Modifier,
value: String,
placeholder: String,
onValueChange: (String) -> Unit,
onSearch: KeyboardActionScope.() -> Unit
) {
BasicTextField(
modifier = modifier,
value = value,
onValueChange = onValueChange,
singleLine = true,
textStyle = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onSurface),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = onSearch),
decorationBox = { innerTextField ->
innerTextField()
// Showing placeholder only when there is query is empty
value.ifEmpty {
Text(
modifier = Modifier.alpha(0.7f),
text = placeholder,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
)
}
}
)
}
@Composable
private fun SearchButton(
onClick: () -> Unit
) {
IconButton(onClick) {
Icon(
Icons.Default.Search,
contentDescription = stringResource(R.string.search_button_description)
)
}
}
@Composable
private fun ClearButton(
visible: Boolean,
onClick: () -> Unit
) {
IconButton(onClick = onClick) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
Icon(
imageVector = Icons.Outlined.Clear,
contentDescription = stringResource(R.string.clear_input_description)
)
}
}
}

View File

@ -18,24 +18,72 @@
package com.sadellie.unitto.timezone.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.timezone.AddTimeZoneRoute
import com.sadellie.unitto.timezone.TimeZoneRoute
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
private val timeZoneRoute: String by lazy { TopLevelDestinations.TimeZone.route }
private val timeZoneGraph: String by lazy { TopLevelDestinations.TimeZone.route }
internal const val timeZoneRoute = "time_zone_route"
internal const val addTimeZoneRoute = "add_time_zone_route"
internal const val userTimeArg = "userTime"
fun NavController.navigateToAddTimeZone(
userTime: ZonedDateTime?
) {
val formattedTime = userTime
?.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
?.replace("/", "|") // this is so wrong
navigate("$addTimeZoneRoute/$formattedTime")
}
fun NavGraphBuilder.timeZoneScreen(
navigateToMenu: () -> Unit,
navigateToSettings: () -> Unit
navigateToSettings: () -> Unit,
navController: NavHostController,
) {
composable(
route = timeZoneRoute,
deepLinks = listOf(
navDeepLink { uriPattern = "app://com.sadellie.unitto/$timeZoneRoute" }
navigation(
startDestination = timeZoneRoute,
route = timeZoneGraph,
deepLinks = listOf(navDeepLink { uriPattern = "app://com.sadellie.unitto/$timeZoneRoute" })
) {
composable(timeZoneRoute) {
TimeZoneRoute(
navigateToMenu = navigateToMenu,
navigateToSettings = navigateToSettings,
navigateToAddTimeZone = navController::navigateToAddTimeZone
)
}
composable(
route = "$addTimeZoneRoute/{$userTimeArg}",
arguments = listOf(
navArgument(userTimeArg) {
defaultValue = null
nullable = true
type = NavType.StringType
}
)
) { stackEntry ->
val userTime = stackEntry.arguments
?.getString(userTimeArg)
?.replace("|", "/") // war crime, don't look
?.let { ZonedDateTime.parse(it, DateTimeFormatter.ISO_ZONED_DATE_TIME) }
AddTimeZoneRoute(
navigateUp = navController::navigateUp,
userTime = userTime
)
) {
TimeZoneRoute()
}
}
}

View File

@ -35,3 +35,4 @@ include(":data:database")
include(":data:model")
include(":data:common")
include(":data:evaluatto")
include(":data:timezone")