mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-18 16:25:27 +02:00
parent
f3ba0bd06a
commit
4fc9fc6b0c
@ -70,17 +70,6 @@ internal fun UnittoApp(prefs: AppPreferences?) {
|
||||
|
||||
val shortcutsScope = rememberCoroutineScope()
|
||||
|
||||
val tabs by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
DrawerItems.Calculator,
|
||||
DrawerItems.Converter,
|
||||
DrawerItems.DateDifference,
|
||||
DrawerItems.TimeZones
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val gesturesEnabled: Boolean by remember(navBackStackEntry?.destination) {
|
||||
derivedStateOf {
|
||||
@ -115,7 +104,7 @@ internal fun UnittoApp(prefs: AppPreferences?) {
|
||||
drawer = {
|
||||
UnittoDrawerSheet(
|
||||
modifier = Modifier,
|
||||
tabs = tabs,
|
||||
tabs = DrawerItems.ALL,
|
||||
currentDestination = navBackStackEntry?.destination?.route
|
||||
) { destination ->
|
||||
drawerScope.launch { drawerState.close() }
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.core.base
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
@ -102,12 +103,20 @@ sealed class TopLevelDestinations(
|
||||
|
||||
// Shown in settings
|
||||
val TOP_LEVEL_DESTINATIONS by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
listOf(
|
||||
TopLevelDestinations.Calculator,
|
||||
TopLevelDestinations.Converter,
|
||||
TopLevelDestinations.DateCalculator,
|
||||
TopLevelDestinations.TimeZone,
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
TopLevelDestinations.Calculator,
|
||||
TopLevelDestinations.Converter,
|
||||
TopLevelDestinations.DateCalculator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Only routes, not graphs!
|
||||
|
@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/QNmUuI88wb1Fm5P8wRIZajZF.png -->
|
||||
<string name="add_label">Add</string>
|
||||
<string name="app_name" translatable="false">Unitto</string>
|
||||
<string name="calculator_clear_history_support">All expressions from history will be deleted forever. This action can\'t be undone!</string>
|
||||
<string name="calculator_divide_by_zero_error">Can\'t divide by 0</string>
|
||||
<string name="calculator_no_history">No history</string>
|
||||
|
||||
<!-- Calculator -->
|
||||
<string name="calculator_title">Calculator</string>
|
||||
<string name="cancel_label">Cancel</string>
|
||||
<string name="checked_filter_description">Checked filter</string>
|
||||
@ -14,8 +15,6 @@
|
||||
<string name="clear_label">Clear</string>
|
||||
<string name="click_to_try_again_label">Click to try again</string>
|
||||
<string name="converter_favorite_button_description">Add or remove unit from favorites</string>
|
||||
|
||||
<!-- Screen names -->
|
||||
<string name="converter_left_side_title">Convert from</string>
|
||||
<string name="converter_no_results_support">Make sure there are no typos, try different filters or check for disabled unit groups.</string>
|
||||
<string name="converter_right_side_title">Convert to</string>
|
||||
@ -43,16 +42,15 @@
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/33QTn2NVrjJT772cBDFRqSMH.png -->
|
||||
<string name="date_calculator_years">Years</string>
|
||||
<string name="delete_label">Delete</string>
|
||||
<string name="disabled_label">Disabled</string>
|
||||
<string name="drop_down_description">Open or close drop down menu</string>
|
||||
<string name="enabled_label">Enabled</string>
|
||||
|
||||
<!-- Tools -->
|
||||
<string name="epoch_converter_title">Epoch converter</string>
|
||||
<string name="error_label">Error</string>
|
||||
<string name="hello_label">Hello!</string>
|
||||
|
||||
<!-- MISC. -->
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/iqEp9rZZ9XqtJ3CKVpy33lre.png -->
|
||||
<string name="label_label">Label</string>
|
||||
<string name="loading_label">Loading…</string>
|
||||
<string name="locale_de" translatable="false">Deutsch</string>
|
||||
<string name="locale_en" translatable="false">English</string>
|
||||
@ -65,8 +63,6 @@
|
||||
<string name="locale_nl" translatable="false">Dutch</string>
|
||||
<string name="locale_ru" translatable="false">Русский</string>
|
||||
<string name="locale_tr" translatable="false">Türkçe</string>
|
||||
|
||||
<!-- Content descriptions -->
|
||||
<string name="navigate_up_description">Navigate up</string>
|
||||
|
||||
<!-- https://s3.eu-west-1.amazonaws.com/po-pub/i/9lSfdkfKShwyQFEF4nvbVaIb.jpg
|
||||
@ -77,7 +73,6 @@ Used in this dialog window. Should be short -->
|
||||
<string name="no_results_label">No results found</string>
|
||||
<string name="ok_label" translatable="false">OK</string>
|
||||
<string name="open_menu_description">Open menu</string>
|
||||
<string name="open_settings_description">Open settings</string>
|
||||
<string name="open_settings_label">Open settings</string>
|
||||
<string name="search_button_description">Search button</string>
|
||||
<string name="search_text_field_placeholder">Search…</string>
|
||||
@ -102,15 +97,11 @@ Used in this dialog window. Should be short -->
|
||||
<string name="settings_dark_mode">Dark</string>
|
||||
<string name="settings_disable_unit_group_description">Disable unit group</string>
|
||||
<string name="settings_display">Display</string>
|
||||
|
||||
<!-- Theme -->
|
||||
<string name="settings_display_support">App look and feel</string>
|
||||
<string name="settings_dynamic_colors">Dynamic colors</string>
|
||||
<string name="settings_dynamic_colors_support">Use colors from your wallpaper</string>
|
||||
<string name="settings_enable_unit_group_description">Enable unit group</string>
|
||||
<string name="settings_exponential_notation">Exponential notation</string>
|
||||
|
||||
<!-- Output format -->
|
||||
<string name="settings_exponential_notation_support">Replace part of the number with E</string>
|
||||
<string name="settings_format_time">Format time</string>
|
||||
<string name="settings_format_time_support">Example: Show 130 minutes as 2h 10m</string>
|
||||
@ -134,8 +125,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="settings_precision">Precision</string>
|
||||
<string name="settings_precision_info">Converted values may have a precision higher than the preferred one.</string>
|
||||
<string name="settings_precision_max">%1$s (Max)</string>
|
||||
|
||||
<!-- Precision -->
|
||||
<string name="settings_precision_support">Number of decimal places</string>
|
||||
<string name="settings_privacy_policy">Privacy Policy</string>
|
||||
<string name="settings_rate_this_app">Rate this app</string>
|
||||
@ -143,14 +132,10 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="settings_selected_color">Selected color</string>
|
||||
<string name="settings_selected_style">Selected style</string>
|
||||
<string name="settings_separator">Separator</string>
|
||||
|
||||
<!-- Separator -->
|
||||
<string name="settings_separator_support">Group separator symbol</string>
|
||||
<string name="settings_sort_by_alphabetical">Alphabetical</string>
|
||||
<string name="settings_sort_by_scale_asc">Scale (Asc.)</string>
|
||||
<string name="settings_sort_by_scale_desc">Scale (Desc.)</string>
|
||||
|
||||
<!-- Units list sorting -->
|
||||
<string name="settings_sort_by_usage">Usage</string>
|
||||
<string name="settings_space">Space</string>
|
||||
<string name="settings_starting_screen">Starting screen</string>
|
||||
@ -162,9 +147,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="settings_system_font">System font</string>
|
||||
<string name="settings_system_font_support">Use system font for texts in app</string>
|
||||
<string name="settings_terms_and_conditions">Terms and Conditions</string>
|
||||
|
||||
<!-- Settings items -->
|
||||
<string name="settings_theme_title">Themes</string>
|
||||
<string name="settings_third_party_licenses">Third party licenses</string>
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_translate_app">Translate this app</string>
|
||||
@ -184,57 +166,35 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_acre_short">ac</string>
|
||||
<string name="unit_angle_minute">Minute</string>
|
||||
<string name="unit_angle_minute_short" translatable="false">\'</string>
|
||||
|
||||
<!-- Angle -->
|
||||
<string name="unit_angle_second">Second</string>
|
||||
<string name="unit_angle_second_short" translatable="false">\"</string>
|
||||
<string name="unit_apostilb">Apostilb</string>
|
||||
<string name="unit_apostilb_short">asb</string>
|
||||
<string name="unit_atomic_mass_unit">Dalton</string>
|
||||
<string name="unit_atomic_mass_unit_short">u</string>
|
||||
|
||||
<!-- ELECTROSTATIC CAPACITANCE -->
|
||||
<string name="unit_attofarad">Attofarad</string>
|
||||
<string name="unit_attofarad_short">aF</string>
|
||||
<string name="unit_attojoule">Attojoule</string>
|
||||
<string name="unit_attojoule_short">aJ</string>
|
||||
|
||||
<!-- Volume -->
|
||||
<string name="unit_attoliter">Attoliter</string>
|
||||
<string name="unit_attoliter_short">aL</string>
|
||||
|
||||
<!-- Length -->
|
||||
<string name="unit_attometer">Attometer</string>
|
||||
|
||||
<!-- Acceleration -->
|
||||
<string name="unit_attometer_per_square_second">Attometer/square second</string>
|
||||
<string name="unit_attometer_per_square_second_short">am/s^2</string>
|
||||
<string name="unit_attometer_short">am</string>
|
||||
<string name="unit_attonewton">Attonewton</string>
|
||||
<string name="unit_attonewton_short">aN</string>
|
||||
|
||||
<!-- Pressure -->
|
||||
<string name="unit_attopascal">Attopascal</string>
|
||||
<string name="unit_attopascal_short">aPa</string>
|
||||
|
||||
<!-- Time -->
|
||||
<string name="unit_attosecond">Attosecond</string>
|
||||
<string name="unit_attosecond_short">as</string>
|
||||
|
||||
<!-- Power -->
|
||||
<string name="unit_attowatt">Attowatt</string>
|
||||
<string name="unit_attowatt_short">aW</string>
|
||||
<string name="unit_bar">Bar</string>
|
||||
<string name="unit_bar_short">bar</string>
|
||||
|
||||
<!-- Number base -->
|
||||
<string name="unit_binary">Binary</string>
|
||||
<string name="unit_binary_short">base2</string>
|
||||
|
||||
<!-- File size -->
|
||||
<string name="unit_bit">Bit</string>
|
||||
|
||||
<!-- Data transfer -->
|
||||
<string name="unit_bit_per_second">Bit/second</string>
|
||||
<string name="unit_bit_per_second_short">b/s</string>
|
||||
<string name="unit_bit_short">b</string>
|
||||
@ -254,14 +214,10 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_candela_per_square_foot_short">cd/ft^2</string>
|
||||
<string name="unit_candela_per_square_inch">Candela/square inch</string>
|
||||
<string name="unit_candela_per_square_inch_short">cd/in^2</string>
|
||||
|
||||
<!-- Luminance -->
|
||||
<string name="unit_candela_per_square_meter">Candela/square meter</string>
|
||||
<string name="unit_candela_per_square_meter_short">cd/m^2</string>
|
||||
<string name="unit_carat">Carat</string>
|
||||
<string name="unit_carat_short">ct</string>
|
||||
|
||||
<!-- Temperature -->
|
||||
<string name="unit_celsius">Celsius</string>
|
||||
<string name="unit_celsius_short" translatable="false">°C</string>
|
||||
<string name="unit_cent">Cent</string>
|
||||
@ -278,8 +234,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_centimeter_short">cm</string>
|
||||
<string name="unit_centipascal">Centipascal</string>
|
||||
<string name="unit_centipascal_short">cPa</string>
|
||||
|
||||
<!-- Tools -->
|
||||
<string name="unit_converter_title">Unit converter</string>
|
||||
<string name="unit_cosmic_velocity_first">First Cosmic Velocity</string>
|
||||
<string name="unit_cosmic_velocity_first_short" translatable="false">v1</string>
|
||||
@ -313,8 +267,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_cubic_millimeter_per_second">Cubic Millimeter/second</string>
|
||||
<string name="unit_cubic_millimeter_per_second_short">mm3/s</string>
|
||||
<string name="unit_cubic_millimeter_short">mm^3</string>
|
||||
|
||||
<!-- Currency -->
|
||||
<string name="unit_currency_1inch" translatable="false">1inch Network</string>
|
||||
<string name="unit_currency_1inch_short" translatable="false">NCH</string>
|
||||
<string name="unit_currency_ada">Cardano</string>
|
||||
@ -765,16 +717,10 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_earth_surface_gravity_short">Earth g</string>
|
||||
<string name="unit_earths_orbital_speed">Earth\'s orbital speed</string>
|
||||
<string name="unit_earths_orbital_speed_short" translatable="false">ve</string>
|
||||
|
||||
<!-- Area -->
|
||||
<string name="unit_electron_cross_section">Electron cross section</string>
|
||||
<string name="unit_electron_cross_section_short">ecs</string>
|
||||
|
||||
<!-- Mass -->
|
||||
<string name="unit_electron_mass_rest">Electron mass</string>
|
||||
<string name="unit_electron_mass_rest_short">me</string>
|
||||
|
||||
<!-- Energy -->
|
||||
<string name="unit_electron_volt">Electron volt</string>
|
||||
<string name="unit_electron_volt_short">eV</string>
|
||||
<string name="unit_energy_horse_power_metric">Horse power</string>
|
||||
@ -881,8 +827,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_group_flux">Flux</string>
|
||||
<string name="unit_group_force">Force</string>
|
||||
<string name="unit_group_fuel_consumption">Fuel</string>
|
||||
|
||||
<!-- Groups -->
|
||||
<string name="unit_group_length">Length</string>
|
||||
<string name="unit_group_luminance">Luminance</string>
|
||||
<string name="unit_group_mass">Mass</string>
|
||||
@ -1018,8 +962,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_light_year">Light year</string>
|
||||
<string name="unit_light_year_short">ly</string>
|
||||
<string name="unit_liter">Liter</string>
|
||||
|
||||
<!-- Flow rate -->
|
||||
<string name="unit_liter_per_hour">Liter/hour</string>
|
||||
<string name="unit_liter_per_hour_short">L/h</string>
|
||||
<string name="unit_liter_per_minute">Liter/minute</string>
|
||||
@ -1043,8 +985,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_mars_mass_short">Mars M</string>
|
||||
<string name="unit_mars_surface_gravity">Mars surface gravity</string>
|
||||
<string name="unit_mars_surface_gravity_short">Mars g</string>
|
||||
|
||||
<!-- Flux -->
|
||||
<string name="unit_maxwell">Maxwell</string>
|
||||
<string name="unit_maxwell_short">Mx</string>
|
||||
<string name="unit_mebibit">Mebibit</string>
|
||||
@ -1144,8 +1084,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_millimeter">Millimeter</string>
|
||||
<string name="unit_millimeter_of_mercury">Millimeter of mercury</string>
|
||||
<string name="unit_millimeter_of_mercury_short">mm Hg</string>
|
||||
|
||||
<!-- Speed -->
|
||||
<string name="unit_millimeter_per_hour">Millimeter/hour</string>
|
||||
<string name="unit_millimeter_per_hour_short">mm/h</string>
|
||||
<string name="unit_millimeter_per_minute">Millimeter/minute</string>
|
||||
@ -1186,13 +1124,9 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_neptune_mass_short">Neptune M</string>
|
||||
<string name="unit_neptune_surface_gravity">Neptune surface gravity</string>
|
||||
<string name="unit_neptune_surface_gravity_short">Neptune g</string>
|
||||
|
||||
<!-- Force -->
|
||||
<string name="unit_newton">Newton</string>
|
||||
<string name="unit_newton_centimeter">Newton centimeter</string>
|
||||
<string name="unit_newton_centimeter_short">N*cm</string>
|
||||
|
||||
<!-- Torque -->
|
||||
<string name="unit_newton_meter">Newton meter</string>
|
||||
<string name="unit_newton_meter_short">N*m</string>
|
||||
<string name="unit_newton_millimeter">Newton millimeter</string>
|
||||
@ -1280,8 +1214,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_prefix_pico_short">p</string>
|
||||
<string name="unit_prefix_quecto">Quecto</string>
|
||||
<string name="unit_prefix_quecto_short">q</string>
|
||||
|
||||
<!-- Prefixes -->
|
||||
<string name="unit_prefix_quetta">Quetta</string>
|
||||
<string name="unit_prefix_quetta_short">Q</string>
|
||||
<string name="unit_prefix_ronna">Ronna</string>
|
||||
@ -1422,8 +1354,6 @@ Maybe this can be labeled better? Let me know. It should be something that can d
|
||||
<string name="unit_yard_per_second">Yard/second</string>
|
||||
<string name="unit_yard_per_second_short">yd/s</string>
|
||||
<string name="unit_yard_short">yd</string>
|
||||
|
||||
<!-- Epoch -->
|
||||
<string name="unit_year_short">y</string>
|
||||
<string name="yesterday">Yesterday</string>
|
||||
</resources>
|
@ -68,7 +68,7 @@ fun UnittoSearchBar(
|
||||
title: String,
|
||||
searchActions: @Composable (RowScope.() -> Unit) = {},
|
||||
noSearchActions: @Composable (RowScope.() -> Unit) = {},
|
||||
placeholder: String,
|
||||
placeholder: String = stringResource(R.string.search_text_field_placeholder),
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors()
|
||||
) {
|
||||
|
@ -36,6 +36,26 @@ data object UnittoDateTimeFormatter {
|
||||
*/
|
||||
val time12Formatter1: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("hh:mm") }
|
||||
|
||||
/**
|
||||
* 23
|
||||
*/
|
||||
val time24OnlyHoursFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("HH") }
|
||||
|
||||
/**
|
||||
* 23
|
||||
*/
|
||||
val time12OnlyHoursFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("hh") }
|
||||
|
||||
/**
|
||||
* 59
|
||||
*/
|
||||
val timeOnlyMinutesFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("mm") }
|
||||
|
||||
/**
|
||||
* 59
|
||||
*/
|
||||
val timeOnlySecondsFormatter: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("ss") }
|
||||
|
||||
/**
|
||||
* AM
|
||||
*/
|
||||
|
@ -27,12 +27,42 @@ import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
* Formats date time into something like:
|
||||
*
|
||||
* 23:59 or 11:59 AM
|
||||
*
|
||||
* Depends on system preferences
|
||||
*
|
||||
* @return Formatted string
|
||||
*/
|
||||
@Composable
|
||||
fun ZonedDateTime.formatLocal(): String {
|
||||
return if (DateFormat.is24HourFormat(LocalContext.current)) format(UnittoDateTimeFormatter.time24Formatter)
|
||||
else format(UnittoDateTimeFormatter.time12FormatterFull)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZonedDateTime.formatOnlyHours(): String {
|
||||
return if (DateFormat.is24HourFormat(LocalContext.current)) format(UnittoDateTimeFormatter.time24OnlyHoursFormatter)
|
||||
else format(UnittoDateTimeFormatter.time12OnlyHoursFormatter)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZonedDateTime.formatOnlyMinutes(): String {
|
||||
return format(UnittoDateTimeFormatter.timeOnlyMinutesFormatter)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZonedDateTime.formatOnlySeconds(): String {
|
||||
return format(UnittoDateTimeFormatter.timeOnlySecondsFormatter)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZonedDateTime.formatOnlyAmPm(): String {
|
||||
return format(UnittoDateTimeFormatter.time12Formatter2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format offset string. Examples:
|
||||
*
|
||||
@ -74,7 +104,7 @@ fun ZonedDateTime.formatOffset(
|
||||
resultBuffer += "${hour}${stringResource(R.string.unit_hour_short)}"
|
||||
}
|
||||
|
||||
// TODO Very ugly
|
||||
// TODO Very ugly. Replace with formatTime option from unit converter
|
||||
if (minute != 0L) {
|
||||
if (hour != 0L) resultBuffer += " "
|
||||
resultBuffer += "${minute}${stringResource(R.string.unit_minute_short)}"
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.core.ui.model
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Calculate
|
||||
import androidx.compose.material.icons.filled.Event
|
||||
@ -58,4 +59,23 @@ sealed class DrawerItems(
|
||||
selectedIcon = Icons.Filled.Schedule,
|
||||
defaultIcon = Icons.Outlined.Schedule
|
||||
)
|
||||
|
||||
companion object {
|
||||
val ALL by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
listOf(
|
||||
Calculator,
|
||||
Converter,
|
||||
DateDifference,
|
||||
TimeZones
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
Calculator,
|
||||
Converter,
|
||||
DateDifference,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,46 @@ val Typography.numbersDisplayMedium by lazy {
|
||||
)
|
||||
}
|
||||
|
||||
val Typography.numberBodyLarge: TextStyle by lazy {
|
||||
TextStyle(
|
||||
fontFamily = latoFamily,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 1.5.em,
|
||||
letterSpacing = 0.5.sp,
|
||||
)
|
||||
}
|
||||
|
||||
val Typography.numberDisplayLarge: TextStyle by lazy {
|
||||
TextStyle(
|
||||
fontFamily = latoFamily,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 1.25.em,
|
||||
letterSpacing = (-0.25).sp,
|
||||
)
|
||||
}
|
||||
|
||||
val Typography.numberHeadlineSmall: TextStyle by lazy {
|
||||
TextStyle(
|
||||
fontFamily = latoFamily,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 1.25.em,
|
||||
letterSpacing = 0.sp,
|
||||
)
|
||||
}
|
||||
|
||||
val Typography.numberHeadlineMedium: TextStyle by lazy {
|
||||
TextStyle(
|
||||
fontFamily = latoFamily,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 1.25.em,
|
||||
letterSpacing = 0.sp,
|
||||
)
|
||||
}
|
||||
|
||||
val TypographyUnitto by lazy {
|
||||
Typography(
|
||||
displayLarge = TextStyle(
|
||||
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.common
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun TimeZone.offset(currentTime: ZonedDateTime): ZonedDateTime {
|
||||
val offsetSeconds = currentTime.offset.totalSeconds.toLong()
|
||||
val currentTimeWithoutOffset = currentTime.minusSeconds(offsetSeconds)
|
||||
|
||||
return currentTimeWithoutOffset.plusSeconds(this.rawOffset / 1000L)
|
||||
}
|
||||
|
||||
val TimeZone.region: String
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
get() = id
|
||||
.replace("_", " ")
|
||||
.split("/")
|
||||
.reversed()
|
||||
.joinToString()
|
@ -19,23 +19,59 @@
|
||||
package com.sadellie.unitto.data.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TimeZoneDao {
|
||||
@Query("SELECT * FROM time_zones ORDER BY position ASC")
|
||||
fun getAll(): Flow<List<TimeZoneEntity>>
|
||||
|
||||
@Query("SELECT * FROM time_zones WHERE position > 0 ORDER BY position ASC")
|
||||
fun getFavorites(): Flow<List<TimeZoneEntity>>
|
||||
|
||||
@Query("SELECT MAX(position) FROM time_zones")
|
||||
fun getMaxPosition(): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg timeZoneEntity: TimeZoneEntity)
|
||||
|
||||
@Query("UPDATE time_zones SET position = ( SELECT SUM(position) FROM time_zones WHERE id IN (:fromId, :toId) ) - position WHERE id IN (:fromId, :toId)")
|
||||
suspend fun swap(fromId: String, toId: String)
|
||||
@Transaction
|
||||
suspend fun addToFavorites(id: String) {
|
||||
insert(
|
||||
TimeZoneEntity(
|
||||
id = id,
|
||||
position = getMaxPosition() + 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Delete
|
||||
suspend fun remove(timeZoneEntity: TimeZoneEntity)
|
||||
@Query("UPDATE time_zones SET position = -1 WHERE id = :id")
|
||||
suspend fun removeFromFavorites(id: String)
|
||||
|
||||
@Query("UPDATE time_zones SET label = :label WHERE id = :id")
|
||||
suspend fun updateLabel(id: String, label: String)
|
||||
|
||||
@Query("UPDATE time_zones SET position = :newPosition WHERE position = :oldPosition AND id = :id")
|
||||
suspend fun updateDragged(id: String, oldPosition: Int, newPosition: Int)
|
||||
|
||||
@Query("UPDATE time_zones SET position = (position - 1) WHERE position > :currentPosition and position <= :targetPosition")
|
||||
suspend fun moveDown(currentPosition: Int, targetPosition: Int)
|
||||
|
||||
@Query("UPDATE time_zones SET position = (position + 1) WHERE position >= :targetPosition AND position < :currentPosition")
|
||||
suspend fun moveUp(currentPosition: Int, targetPosition: Int)
|
||||
|
||||
@Transaction
|
||||
suspend fun moveMove(id: String, currentPosition: Int, targetPosition: Int) {
|
||||
// Very good explanation
|
||||
// https://www.c-sharpcorner.com/article/updating-display-order-in-database-with-drag-drop5/
|
||||
updateDragged(id, currentPosition, 0)
|
||||
if (targetPosition > currentPosition) {
|
||||
moveDown(currentPosition, targetPosition)
|
||||
} else {
|
||||
moveUp(currentPosition, targetPosition)
|
||||
}
|
||||
updateDragged(id, 0, targetPosition)
|
||||
}
|
||||
}
|
||||
|
@ -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.model.timezone
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
data class FavoriteZone(
|
||||
val timeZone: TimeZone,
|
||||
val position: Int,
|
||||
val label: String
|
||||
)
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.model.timezone
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
|
||||
/**
|
||||
* Don't get 'region' from [timeZone]. Use [formattedLabel] (same but cached)
|
||||
*/
|
||||
data class SearchResultZone(
|
||||
val timeZone: TimeZone,
|
||||
val formattedLabel: String
|
||||
)
|
@ -18,97 +18,143 @@
|
||||
|
||||
package com.sadellie.unitto.data.timezone
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.sadellie.unitto.data.common.lev
|
||||
import com.sadellie.unitto.data.common.region
|
||||
import com.sadellie.unitto.data.database.TimeZoneDao
|
||||
import com.sadellie.unitto.data.database.TimeZoneEntity
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import com.sadellie.unitto.data.model.timezone.FavoriteZone
|
||||
import com.sadellie.unitto.data.model.timezone.SearchResultZone
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Singleton
|
||||
class TimeZonesRepository @Inject constructor(
|
||||
private val dao: TimeZoneDao
|
||||
) {
|
||||
private val allTimeZones: HashMap<String, UnittoTimeZone> = hashMapOf(
|
||||
"zulu_time_zone" to UnittoTimeZone(id = "zulu_time_zone", nameRes = "Zulu Time Zone", offsetSeconds = 0)
|
||||
)
|
||||
// Not implemented because it will take me too much time to map 600+ TimeZones and codes
|
||||
// private val codeToTimeZoneId: HashMap<String, String> by lazy {
|
||||
// hashMapOf()
|
||||
// }
|
||||
|
||||
val favoriteTimeZones: Flow<List<UnittoTimeZone>> = dao
|
||||
.getAll()
|
||||
val favoriteTimeZones: Flow<List<FavoriteZone>> = dao
|
||||
.getFavorites()
|
||||
.map { list ->
|
||||
val favorites = mutableListOf<UnittoTimeZone>()
|
||||
val favorites = mutableListOf<FavoriteZone>()
|
||||
list.forEach { entity ->
|
||||
val foundTimeZone = allTimeZones[entity.id] ?: return@forEach
|
||||
val mapped = foundTimeZone.copy(
|
||||
position = entity.position
|
||||
favorites.add(
|
||||
FavoriteZone(
|
||||
timeZone = TimeZone.getTimeZone(entity.id),
|
||||
position = entity.position,
|
||||
label = entity.label
|
||||
)
|
||||
)
|
||||
favorites.add(mapped)
|
||||
}
|
||||
|
||||
favorites
|
||||
}
|
||||
|
||||
suspend fun swapTimeZones(from: String, to: String) = withContext(Dispatchers.IO) {
|
||||
dao.swap(from, to)
|
||||
|
||||
return@withContext
|
||||
suspend fun moveTimeZone(
|
||||
timeZone: FavoriteZone,
|
||||
targetPosition: Int
|
||||
) = withContext(Dispatchers.IO) {
|
||||
dao.moveMove(timeZone.timeZone.id, timeZone.position, targetPosition)
|
||||
}
|
||||
|
||||
suspend fun delete(timeZone: UnittoTimeZone) = withContext(Dispatchers.IO) {
|
||||
// Only PrimaryKey is needed
|
||||
dao.remove(TimeZoneEntity(id = timeZone.id, position = 0))
|
||||
}
|
||||
|
||||
suspend fun filterAllTimeZones(searchQuery: String): List<UnittoTimeZone> =
|
||||
suspend fun addToFavorites(
|
||||
timeZone: TimeZone
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
dao.addToFavorites(timeZone.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeFromFavorites(
|
||||
timeZone: FavoriteZone
|
||||
) = withContext(Dispatchers.IO) {
|
||||
dao.removeFromFavorites(timeZone.timeZone.id)
|
||||
}
|
||||
|
||||
suspend fun updateLabel(
|
||||
timeZone: FavoriteZone,
|
||||
label: String
|
||||
) = withContext(Dispatchers.IO) {
|
||||
dao.updateLabel(timeZone.timeZone.id, label)
|
||||
}
|
||||
|
||||
suspend fun filterAllTimeZones(
|
||||
searchQuery: String
|
||||
): List<SearchResultZone> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val favorites = dao.getFavorites().first().map { it.id }
|
||||
|
||||
val query = searchQuery.trim().lowercase()
|
||||
val threshold: Int = query.length / 2
|
||||
val timeZonesWithDist = mutableListOf<Pair<UnittoTimeZone, Int>>()
|
||||
val timeZonesWithDist = mutableListOf<Pair<SearchResultZone, Int>>()
|
||||
|
||||
allTimeZones.values.forEach { timeZone ->
|
||||
val timeZoneName = timeZone.nameRes
|
||||
TimeZone.getAvailableIDs().forEach { timeZoneId ->
|
||||
if (timeZoneId in favorites) return@forEach
|
||||
|
||||
if (timeZone.code.lowercase() == query) {
|
||||
timeZonesWithDist.add(timeZone to 1)
|
||||
return@forEach
|
||||
}
|
||||
val timeZone = TimeZone.getTimeZone(timeZoneId)
|
||||
val name = timeZone.displayName
|
||||
val id = timeZone.region
|
||||
|
||||
// // CODE Match
|
||||
// if (codeToTimeZoneId[timeZone.id]?.lowercase() == query) {
|
||||
// timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1)
|
||||
// return@forEach
|
||||
// }
|
||||
|
||||
// Display name match
|
||||
when {
|
||||
// not zero, so that lev can have that
|
||||
timeZoneName.startsWith(query) -> {
|
||||
timeZonesWithDist.add(timeZone to 1)
|
||||
name.startsWith(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
timeZoneName.contains(query) -> {
|
||||
timeZonesWithDist.add(timeZone to 2)
|
||||
name.contains(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
val levDist = timeZoneName
|
||||
.substring(0, minOf(query.length, timeZoneName.length))
|
||||
val nameLevDist = name
|
||||
.substring(0, minOf(query.length, name.length))
|
||||
.lev(query)
|
||||
if (nameLevDist < threshold) {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, id) to nameLevDist)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (levDist < threshold) {
|
||||
timeZonesWithDist.add(timeZone to levDist)
|
||||
// ID Match
|
||||
when {
|
||||
// not zero, so that lev can have that
|
||||
id.startsWith(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 1)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
id.contains(query) -> {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, id) to 2)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
val idLevDist = id
|
||||
.substring(0, minOf(query.length, id.length))
|
||||
.lev(query)
|
||||
if (idLevDist < threshold) {
|
||||
timeZonesWithDist.add(SearchResultZone(timeZone, id) to idLevDist)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext timeZonesWithDist.sortedBy { it.second }.map { it.first }
|
||||
}
|
||||
|
||||
suspend fun addToFavorites(timeZone: UnittoTimeZone) {
|
||||
// UNCOMMENT FOR RELEASE
|
||||
dao.insert(
|
||||
TimeZoneEntity(
|
||||
id = timeZone.id,
|
||||
position = System.currentTimeMillis().toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ internal fun ChipsRow(
|
||||
AssistChip(
|
||||
onClick = navigateToSettingsAction,
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = stringResource(R.string.open_settings_description)
|
||||
contentDescription = stringResource(R.string.open_settings_label)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -159,7 +159,7 @@ fun ChipsFlexRow(
|
||||
AssistChip(
|
||||
onClick = navigateToSettingsAction,
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = stringResource(R.string.open_settings_description)
|
||||
contentDescription = stringResource(R.string.open_settings_label)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ internal fun SearchPlaceholder(navigateToSettingsAction: () -> Unit) {
|
||||
)
|
||||
// Open settings button
|
||||
ElevatedButton(onClick = navigateToSettingsAction) {
|
||||
Text(text = stringResource(R.string.open_settings_description))
|
||||
Text(text = stringResource(R.string.open_settings_label))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ android {
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.junit.junit)
|
||||
testImplementation(libs.org.robolectric.robolectric)
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
implementation(libs.com.github.sadellie.themmo)
|
||||
implementation(libs.org.burnoutcrew.composereorderable.reorderable)
|
||||
|
||||
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.feature.timezone.components
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import com.sadellie.unitto.data.model.timezone.FavoriteZone
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class FavoriteTimeZonesTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun convertTime(): Unit = with(composeTestRule) {
|
||||
setContent {
|
||||
FavoriteTimeZoneItem(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
item = FavoriteZone(
|
||||
timeZone = TimeZone.getTimeZone("Africa/Addis_Ababa"),
|
||||
position = -1,
|
||||
label = "label text"
|
||||
),
|
||||
fromTime = ZonedDateTime.parse(
|
||||
"2023-05-01T14:00+03:00[Africa/Addis_Ababa]",
|
||||
DateTimeFormatter.ISO_ZONED_DATE_TIME
|
||||
),
|
||||
expanded = true,
|
||||
onClick = {},
|
||||
onDelete = {},
|
||||
onPrimaryClick = {},
|
||||
onLabelClick = {},
|
||||
isDragging = false
|
||||
)
|
||||
}
|
||||
|
||||
onNodeWithText("11:00").assertExists()
|
||||
}
|
||||
|
||||
}
|
@ -18,138 +18,130 @@
|
||||
|
||||
package com.sadellie.unitto.feature.timezone
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.sadellie.unitto.core.base.R
|
||||
import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen
|
||||
import com.sadellie.unitto.core.ui.common.UnittoListItem
|
||||
import com.sadellie.unitto.core.ui.common.UnittoSearchBar
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import com.sadellie.unitto.feature.timezone.components.SelectableTimeZone
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import com.sadellie.unitto.core.ui.datetime.formatLocal
|
||||
import com.sadellie.unitto.core.ui.theme.numberHeadlineSmall
|
||||
import com.sadellie.unitto.data.common.offset
|
||||
import com.sadellie.unitto.data.common.region
|
||||
import com.sadellie.unitto.data.model.timezone.SearchResultZone
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Composable
|
||||
internal fun AddTimeZoneRoute(
|
||||
viewModel: AddTimeZoneViewModel = hiltViewModel(),
|
||||
navigateUp: () -> Unit,
|
||||
userTime: ZonedDateTime? = null
|
||||
userTime: ZonedDateTime,
|
||||
) {
|
||||
val uiState = viewModel.addTimeZoneUIState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (userTime == null) {
|
||||
while (isActive) {
|
||||
viewModel.setTime(ZonedDateTime.now())
|
||||
delay(1000)
|
||||
}
|
||||
} else {
|
||||
viewModel.setTime(userTime)
|
||||
}
|
||||
}
|
||||
|
||||
AddTimeZoneScreen(
|
||||
uiState = uiState.value,
|
||||
when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) {
|
||||
AddTimeZoneUIState.Loading -> UnittoEmptyScreen()
|
||||
is AddTimeZoneUIState.Ready -> AddTimeZoneScreen(
|
||||
uiState = uiState,
|
||||
navigateUp = navigateUp,
|
||||
onQueryChange = viewModel::onQueryChange,
|
||||
addToFavorites = viewModel::addToFavorites,
|
||||
userTime = userTime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Composable
|
||||
fun AddTimeZoneScreen(
|
||||
uiState: AddTimeZoneUIState,
|
||||
uiState: AddTimeZoneUIState.Ready,
|
||||
navigateUp: () -> Unit,
|
||||
onQueryChange: (TextFieldValue) -> Unit,
|
||||
addToFavorites: (UnittoTimeZone) -> Unit,
|
||||
addToFavorites: (TimeZone) -> Unit,
|
||||
userTime: ZonedDateTime,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
|
||||
rememberTopAppBarState()
|
||||
)
|
||||
val elevatedColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||
val needToTint by remember {
|
||||
derivedStateOf { scrollBehavior.state.overlappedFraction > 0.01f }
|
||||
}
|
||||
|
||||
val searchBarBackground = animateColorAsState(
|
||||
targetValue = if (needToTint) elevatedColor else MaterialTheme.colorScheme.surface,
|
||||
animationSpec = tween(durationMillis = 500, easing = LinearOutSlowInEasing),
|
||||
label = "Search bar background"
|
||||
)
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
UnittoSearchBar(
|
||||
modifier = Modifier,
|
||||
query = uiState.query,
|
||||
onQueryChange = onQueryChange,
|
||||
navigateUp = navigateUp,
|
||||
title = stringResource(R.string.time_zone_add_title),
|
||||
placeholder = stringResource(R.string.search_text_field_placeholder),
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = searchBarBackground.value
|
||||
)
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
state = listState
|
||||
) {
|
||||
items(uiState.list) {
|
||||
SelectableTimeZone(
|
||||
timeZone = it,
|
||||
Crossfade(targetState = uiState.list.isEmpty()) { empty ->
|
||||
if (empty) {
|
||||
UnittoEmptyScreen()
|
||||
} else {
|
||||
LazyColumn(contentPadding = paddingValues) {
|
||||
items(uiState.list, { it.timeZone.id }) {
|
||||
UnittoListItem(
|
||||
modifier = Modifier
|
||||
.clickable { addToFavorites(it); navigateUp() }
|
||||
.fillMaxWidth(),
|
||||
currentTime = uiState.userTime
|
||||
.animateItemPlacement()
|
||||
.clickable {
|
||||
addToFavorites(it.timeZone)
|
||||
navigateUp()
|
||||
},
|
||||
headlineContent = { Text(it.timeZone.displayName) },
|
||||
supportingContent = { Text(it.formattedLabel) },
|
||||
trailingContent = {
|
||||
Text(
|
||||
text = it.timeZone.offset(userTime).formatLocal(),
|
||||
style = MaterialTheme.typography.numberHeadlineSmall
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewAddTimeZoneScreen() {
|
||||
AddTimeZoneScreen(
|
||||
navigateUp = {},
|
||||
uiState = AddTimeZoneUIState(
|
||||
list = List(50) {
|
||||
UnittoTimeZone(
|
||||
id = "timezone $it",
|
||||
nameRes = "Time zone $it",
|
||||
uiState = AddTimeZoneUIState.Ready(
|
||||
query = TextFieldValue(),
|
||||
list = listOf(
|
||||
"UTC",
|
||||
"Africa/Addis_Ababa",
|
||||
"ACT"
|
||||
).map {
|
||||
val zone = TimeZone.getTimeZone(it)
|
||||
SearchResultZone(
|
||||
timeZone = zone,
|
||||
formattedLabel = zone.region
|
||||
)
|
||||
}
|
||||
),
|
||||
navigateUp = {},
|
||||
onQueryChange = {},
|
||||
addToFavorites = {},
|
||||
userTime = ZonedDateTime.now()
|
||||
)
|
||||
}
|
||||
|
@ -19,11 +19,13 @@
|
||||
package com.sadellie.unitto.feature.timezone
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import java.time.ZonedDateTime
|
||||
import com.sadellie.unitto.data.model.timezone.SearchResultZone
|
||||
|
||||
data class AddTimeZoneUIState(
|
||||
val query: TextFieldValue = TextFieldValue(),
|
||||
val list: List<UnittoTimeZone> = emptyList(),
|
||||
val userTime: ZonedDateTime? = null,
|
||||
)
|
||||
sealed class AddTimeZoneUIState {
|
||||
data object Loading: AddTimeZoneUIState()
|
||||
|
||||
data class Ready(
|
||||
val query: TextFieldValue,
|
||||
val list: List<SearchResultZone>,
|
||||
): AddTimeZoneUIState()
|
||||
}
|
||||
|
@ -18,66 +18,52 @@
|
||||
|
||||
package com.sadellie.unitto.feature.timezone
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import com.sadellie.unitto.data.common.stateIn
|
||||
import com.sadellie.unitto.data.model.timezone.SearchResultZone
|
||||
import com.sadellie.unitto.data.timezone.TimeZonesRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.ZonedDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@HiltViewModel
|
||||
class AddTimeZoneViewModel @Inject constructor(
|
||||
private val timezonesRepository: TimeZonesRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _userTime = MutableStateFlow<ZonedDateTime?>(ZonedDateTime.now())
|
||||
private val _query = MutableStateFlow(TextFieldValue())
|
||||
private val _result = MutableStateFlow(emptyList<SearchResultZone>())
|
||||
|
||||
private val _filteredTimeZones = MutableStateFlow(emptyList<UnittoTimeZone>())
|
||||
val addTimeZoneUIState = combine(
|
||||
val uiState = combine(
|
||||
_query,
|
||||
_filteredTimeZones,
|
||||
_userTime,
|
||||
) { query, filteredTimeZone, userTime ->
|
||||
return@combine AddTimeZoneUIState(
|
||||
_result,
|
||||
timezonesRepository.favoriteTimeZones,
|
||||
) { query, result, _ ->
|
||||
return@combine AddTimeZoneUIState.Ready(
|
||||
query = query,
|
||||
list = filteredTimeZone,
|
||||
userTime = userTime,
|
||||
list = result,
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(5000), AddTimeZoneUIState()
|
||||
)
|
||||
|
||||
fun onQueryChange(query: TextFieldValue) {
|
||||
_query.update { query }
|
||||
filterTimeZones(query.text)
|
||||
}
|
||||
|
||||
private fun filterTimeZones(query: String = "") = viewModelScope.launch {
|
||||
_filteredTimeZones.update {
|
||||
timezonesRepository.filterAllTimeZones(query)
|
||||
.mapLatest { ui ->
|
||||
viewModelScope.launch {
|
||||
_result.update { timezonesRepository.filterAllTimeZones(ui.query.text) }
|
||||
}
|
||||
ui
|
||||
}
|
||||
.stateIn(viewModelScope, AddTimeZoneUIState.Loading)
|
||||
|
||||
fun addToFavorites(timeZone: UnittoTimeZone) = viewModelScope.launch(Dispatchers.IO) {
|
||||
fun onQueryChange(textFieldValue: TextFieldValue) = _query.update { textFieldValue }
|
||||
|
||||
fun addToFavorites(timeZone: TimeZone) = viewModelScope.launch {
|
||||
timezonesRepository.addToFavorites(timeZone)
|
||||
}
|
||||
|
||||
fun setTime(time: ZonedDateTime) = viewModelScope.launch(Dispatchers.Default) {
|
||||
_userTime.update { time }
|
||||
}
|
||||
|
||||
init {
|
||||
// TODO Maybe only when actually needed?
|
||||
filterTimeZones()
|
||||
}
|
||||
}
|
||||
|
@ -16,22 +16,18 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.data.model
|
||||
package com.sadellie.unitto.feature.timezone
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
data class UnittoTimeZone(
|
||||
val id: String,
|
||||
// For beta only, will change to StringRes later
|
||||
val nameRes: String,
|
||||
val position: Int = 0,
|
||||
val offsetSeconds: Long = 0,
|
||||
val code: String = "CODE",
|
||||
) {
|
||||
fun offsetFrom(currentTime: ZonedDateTime): ZonedDateTime {
|
||||
val offsetSeconds = currentTime.offset.totalSeconds.toLong()
|
||||
val currentTimeWithoutOffset = currentTime.minusSeconds(offsetSeconds)
|
||||
val TimeZone.offsetSeconds
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
get() = this.rawOffset / 1000L
|
||||
|
||||
return currentTimeWithoutOffset.plusSeconds(this.offsetSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun TimeZone.timeNow(): ZonedDateTime = ZonedDateTime.now(ZoneId.of(this.id, ZoneId.SHORT_IDS))
|
@ -18,61 +18,52 @@
|
||||
|
||||
package com.sadellie.unitto.feature.timezone
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.animateColor
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.animateInt
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.animateTo
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeFloatingActionButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@ -81,102 +72,112 @@ import com.sadellie.unitto.core.base.R
|
||||
import com.sadellie.unitto.core.ui.common.MenuButton
|
||||
import com.sadellie.unitto.core.ui.common.SettingsButton
|
||||
import com.sadellie.unitto.core.ui.common.TimePickerDialog
|
||||
import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen
|
||||
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
|
||||
import com.sadellie.unitto.core.ui.common.squashable
|
||||
import com.sadellie.unitto.core.ui.datetime.UnittoDateTimeFormatter
|
||||
import com.sadellie.unitto.core.ui.datetime.formatLocal
|
||||
import com.sadellie.unitto.core.ui.theme.TypographyUnitto
|
||||
import com.sadellie.unitto.core.ui.theme.DarkThemeColors
|
||||
import com.sadellie.unitto.core.ui.theme.LightThemeColors
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import com.sadellie.unitto.feature.timezone.components.TimeZoneListItem
|
||||
import io.github.sadellie.themmo.Themmo
|
||||
import io.github.sadellie.themmo.rememberThemmoController
|
||||
import com.sadellie.unitto.data.model.timezone.FavoriteZone
|
||||
import com.sadellie.unitto.feature.timezone.components.FavoriteTimeZoneItem
|
||||
import com.sadellie.unitto.feature.timezone.components.UserTimeZone
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||
import org.burnoutcrew.reorderable.reorderable
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Composable
|
||||
internal fun TimeZoneRoute(
|
||||
viewModel: TimeZoneViewModel = hiltViewModel(),
|
||||
navigateToMenu: () -> Unit,
|
||||
openMenu: () -> Unit,
|
||||
navigateToSettings: () -> Unit,
|
||||
navigateToAddTimeZone: (ZonedDateTime?) -> Unit,
|
||||
navigateToAddTimeZone: (ZonedDateTime) -> Unit,
|
||||
) {
|
||||
val uiState = viewModel.timeZoneUIState.collectAsStateWithLifecycle()
|
||||
|
||||
when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) {
|
||||
TimeZoneUIState.Loading -> UnittoEmptyScreen()
|
||||
is TimeZoneUIState.Ready -> {
|
||||
TimeZoneScreen(
|
||||
uiState = uiState.value,
|
||||
navigateToMenu = navigateToMenu,
|
||||
uiState = uiState,
|
||||
openMenu = openMenu,
|
||||
navigateToSettings = navigateToSettings,
|
||||
navigateToAddTimeZone = {
|
||||
navigateToAddTimeZone(
|
||||
if (uiState.value.updateTime) null
|
||||
else uiState.value.userTime
|
||||
)
|
||||
},
|
||||
onDragEnd = viewModel::onDragEnd,
|
||||
onDelete = viewModel::onDelete,
|
||||
setSelectedTime = viewModel::setCustomTime,
|
||||
navigateToAddTimeZone = navigateToAddTimeZone,
|
||||
setCurrentTime = viewModel::setCurrentTime,
|
||||
resetTime = viewModel::resetTime,
|
||||
setSelectedTime = viewModel::setSelectedTime,
|
||||
onDragEnd = viewModel::onDragEnd,
|
||||
delete = viewModel::delete,
|
||||
updateLabel = viewModel::updateLabel,
|
||||
selectTimeZone = viewModel::selectTimeZone,
|
||||
setDialogState = viewModel::setDialogState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Composable
|
||||
private fun TimeZoneScreen(
|
||||
uiState: TimeZoneUIState,
|
||||
navigateToMenu: () -> Unit,
|
||||
uiState: TimeZoneUIState.Ready,
|
||||
openMenu: () -> Unit,
|
||||
navigateToSettings: () -> Unit,
|
||||
navigateToAddTimeZone: () -> Unit,
|
||||
onDragEnd: (String, String) -> Unit,
|
||||
onDelete: (UnittoTimeZone) -> Unit,
|
||||
setSelectedTime: (ZonedDateTime) -> Unit,
|
||||
navigateToAddTimeZone: (ZonedDateTime) -> Unit,
|
||||
setCurrentTime: () -> Unit,
|
||||
resetTime: () -> Unit,
|
||||
setSelectedTime: (ZonedDateTime) -> Unit,
|
||||
onDragEnd: (id: FavoriteZone, target: Int) -> Unit,
|
||||
delete: (FavoriteZone) -> Unit,
|
||||
updateLabel: (FavoriteZone, String) -> Unit,
|
||||
selectTimeZone: (FavoriteZone?) -> Unit,
|
||||
setDialogState: (TimeZoneDialogState) -> Unit,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
var currentUserTime by remember(uiState.customUserTime) {
|
||||
mutableStateOf(
|
||||
uiState.customUserTime ?: uiState.userTimeZone.timeNow()
|
||||
)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(uiState.updateTime) {
|
||||
while (uiState.updateTime and isActive) {
|
||||
setCurrentTime()
|
||||
LaunchedEffect(uiState.customUserTime) {
|
||||
while ((uiState.customUserTime == null) and isActive) {
|
||||
currentUserTime = uiState.userTimeZone.timeNow()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
val copiedList = rememberUpdatedState(newValue = uiState.list) as MutableState
|
||||
val copiedList = rememberUpdatedState(newValue = uiState.favorites) as MutableState
|
||||
val state = rememberReorderableLazyListState(
|
||||
onMove = onMove@{ from, to ->
|
||||
onMove = { from, to ->
|
||||
// -1 because we use fake item. It fixes animation for the first item in list
|
||||
copiedList.value = copiedList.value
|
||||
.toMutableList()
|
||||
.apply {
|
||||
add(to.index - 1, removeAt(from.index - 1))
|
||||
}
|
||||
onDragEnd(from.key as String, to.key as String)
|
||||
},
|
||||
canDragOver = { draggedOver, _ ->
|
||||
// Don't allow dragging over fake item
|
||||
draggedOver.index > 0
|
||||
},
|
||||
onDragEnd = onDragEnd@{ from, to ->
|
||||
if (from == to) return@onDragEnd
|
||||
// There is some logic going on. I have no idea what I did here but it works
|
||||
val tz = copiedList.value.getOrNull(to - 1) ?: return@onDragEnd
|
||||
val targetInOldTz = uiState.favorites.getOrNull(to - 1) ?: return@onDragEnd
|
||||
|
||||
onDragEnd(tz, targetInOldTz.position)
|
||||
}
|
||||
)
|
||||
// TODO Unswipe on dragging
|
||||
var swiped by remember<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_title)) },
|
||||
navigationIcon = { MenuButton(navigateToMenu) },
|
||||
navigationIcon = { MenuButton(openMenu) },
|
||||
actions = { SettingsButton(navigateToSettings) },
|
||||
floatingActionButton = {
|
||||
LargeFloatingActionButton(navigateToAddTimeZone) {
|
||||
LargeFloatingActionButton(
|
||||
onClick = {
|
||||
navigateToAddTimeZone(currentUserTime)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = null,
|
||||
@ -186,194 +187,183 @@ private fun TimeZoneScreen(
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
||||
) { paddingValues ->
|
||||
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
state = state.listState,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxHeight()
|
||||
.reorderable(state)
|
||||
.detectReorderAfterLongPress(state),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(bottom = 124.dp)
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.reorderable(state),
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 124.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// This is a fake item. First item in list can not animated, so we do this magic fuckery
|
||||
item {
|
||||
item("user time") {
|
||||
UserTimeZone(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
userTime = uiState.userTime,
|
||||
onClick = { showTimeSelector = true },
|
||||
onResetClick = resetTime,
|
||||
showReset = !uiState.updateTime,
|
||||
.padding(8.dp),
|
||||
userTime = currentUserTime,
|
||||
onClick = { setDialogState(TimeZoneDialogState.UserTimePicker(currentUserTime)) },
|
||||
onResetClick = setCurrentTime,
|
||||
showReset = uiState.customUserTime != null
|
||||
)
|
||||
}
|
||||
items(copiedList.value, { it.id }) { item ->
|
||||
ReorderableItem(state, key = item.id) { isDragging ->
|
||||
|
||||
items(copiedList.value, { it.timeZone.id }) { item ->
|
||||
ReorderableItem(
|
||||
reorderableState = state,
|
||||
key = item.timeZone.id,
|
||||
) { isDragging ->
|
||||
val isSelected = uiState.selectedTimeZone == item
|
||||
|
||||
val transition = updateTransition(isDragging, label = "draggedTransition")
|
||||
val background by transition.animateColor(label = "background") {
|
||||
if (it) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
val itemPadding by transition.animateDp(label = "itemPadding") {
|
||||
if (it) 32.dp else 16.dp
|
||||
if (it) 8.dp else 0.dp
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val draggableState = rememberDraggableTimeZone(maxDrag) { swiped = item }
|
||||
|
||||
LaunchedEffect(swiped) {
|
||||
if (swiped != item) scope.launch {
|
||||
draggableState.animateTo(false)
|
||||
}
|
||||
val elevation by transition.animateDp(label = "elevation") {
|
||||
if (it) 8.dp else 2.dp
|
||||
}
|
||||
|
||||
TimeZoneListItem(
|
||||
val cornerRadius by transition.animateInt(label = "cornerRadius") {
|
||||
if (it) 25 else 15
|
||||
}
|
||||
|
||||
FavoriteTimeZoneItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = itemPadding),
|
||||
timeZone = item,
|
||||
currentTime = uiState.userTime,
|
||||
onDelete = onDelete,
|
||||
color = background,
|
||||
onSwipe = {},
|
||||
draggableState = draggableState,
|
||||
.padding(itemPadding)
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(elevation))
|
||||
.detectReorderAfterLongPress(state),
|
||||
item = item,
|
||||
fromTime = currentUserTime,
|
||||
expanded = isSelected,
|
||||
onClick = {
|
||||
selectTimeZone(if (isSelected) null else item)
|
||||
},
|
||||
onDelete = { delete(item) },
|
||||
onLabelClick = { setDialogState(TimeZoneDialogState.LabelEditPicker(item)) },
|
||||
onPrimaryClick = { offsetTime ->
|
||||
setDialogState(TimeZoneDialogState.FavoriteTimePicker(item, offsetTime))
|
||||
},
|
||||
isDragging = isDragging
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showTimeSelector) {
|
||||
when (uiState.dialogState) {
|
||||
is TimeZoneDialogState.UserTimePicker -> {
|
||||
TimePickerDialog(
|
||||
hour = uiState.userTime.hour,
|
||||
minute = uiState.userTime.minute,
|
||||
hour = currentUserTime.hour,
|
||||
minute = currentUserTime.minute,
|
||||
onConfirm = { hour, minute ->
|
||||
setSelectedTime(
|
||||
uiState.userTime
|
||||
currentUserTime
|
||||
.withHour(hour)
|
||||
.withMinute(minute)
|
||||
)
|
||||
showTimeSelector = false
|
||||
setDialogState(TimeZoneDialogState.Nothing)
|
||||
},
|
||||
onDismiss = { showTimeSelector = false }
|
||||
onDismiss = { setDialogState(TimeZoneDialogState.Nothing) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserTimeZone(
|
||||
modifier: Modifier,
|
||||
userTime: ZonedDateTime,
|
||||
onClick: () -> Unit,
|
||||
onResetClick: () -> Unit,
|
||||
showReset: Boolean,
|
||||
) {
|
||||
is TimeZoneDialogState.FavoriteTimePicker -> {
|
||||
TimePickerDialog(
|
||||
hour = uiState.dialogState.time.hour,
|
||||
minute = uiState.dialogState.time.minute,
|
||||
onConfirm = { hour, minute ->
|
||||
setSelectedTime(
|
||||
uiState.dialogState.time
|
||||
.withHour(hour)
|
||||
.withMinute(minute)
|
||||
.minusSeconds(uiState.dialogState.timeZone.timeZone.offsetSeconds)
|
||||
.plusSeconds(uiState.userTimeZone.offsetSeconds)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.squashable(
|
||||
onClick = onClick,
|
||||
onLongClick = onResetClick,
|
||||
cornerRadiusRange = 8.dp..32.dp,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.tertiaryContainer)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = userTime.format(UnittoDateTimeFormatter.zoneFormatPattern),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = userTime.formatLocal(),
|
||||
label = "user time change",
|
||||
transitionSpec = {
|
||||
slideInVertically { height -> height } + fadeIn() togetherWith
|
||||
slideOutVertically { height -> -height } + fadeOut() using
|
||||
SizeTransform()
|
||||
}
|
||||
) { time ->
|
||||
Text(
|
||||
text = time,
|
||||
style = MaterialTheme.typography.displayLarge,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = userTime.format(UnittoDateTimeFormatter.dayMonthYear),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showReset,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut(),
|
||||
) {
|
||||
IconButton(onResetClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.History,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberDraggableTimeZone(
|
||||
maxDrag: Float,
|
||||
onSwipe: () -> Unit,
|
||||
) = remember {
|
||||
AnchoredDraggableState(
|
||||
initialValue = false,
|
||||
anchors = DraggableAnchors {
|
||||
false at 0f
|
||||
true at maxDrag
|
||||
setDialogState(TimeZoneDialogState.Nothing)
|
||||
},
|
||||
positionalThreshold = { it * 0.5f },
|
||||
velocityThreshold = { maxDrag },
|
||||
animationSpec = tween(),
|
||||
confirmValueChange = {
|
||||
onSwipe()
|
||||
true
|
||||
}
|
||||
onDismiss = { setDialogState(TimeZoneDialogState.Nothing) }
|
||||
)
|
||||
}
|
||||
|
||||
is TimeZoneDialogState.LabelEditPicker -> {
|
||||
var tfv by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(
|
||||
TextFieldValue(
|
||||
text = uiState.dialogState.timeZone.label,
|
||||
selection = TextRange(uiState.dialogState.timeZone.label.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.label_label)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = tfv,
|
||||
onValueChange = { tfv = it },
|
||||
modifier = Modifier.focusRequester(focusRequester)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
awaitFrame()
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
setDialogState(TimeZoneDialogState.Nothing)
|
||||
updateLabel(uiState.dialogState.timeZone, tfv.text)
|
||||
},
|
||||
content = { Text(text = stringResource(R.string.ok_label)) }
|
||||
)
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { setDialogState(TimeZoneDialogState.Nothing) },
|
||||
content = { Text(text = stringResource(R.string.cancel_label)) }
|
||||
)
|
||||
},
|
||||
onDismissRequest = { setDialogState(TimeZoneDialogState.Nothing) },
|
||||
)
|
||||
}
|
||||
|
||||
TimeZoneDialogState.Nothing -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTimeZoneScreen() {
|
||||
Themmo(
|
||||
themmoController = rememberThemmoController(
|
||||
lightColorScheme = LightThemeColors,
|
||||
darkColorScheme = DarkThemeColors,
|
||||
),
|
||||
typography = TypographyUnitto,
|
||||
) {
|
||||
TimeZoneScreen(
|
||||
uiState = TimeZoneUIState(
|
||||
list = List(50) {
|
||||
UnittoTimeZone(
|
||||
id = "timezone $it",
|
||||
nameRes = "Time zone $it",
|
||||
uiState = TimeZoneUIState.Ready(
|
||||
favorites = TimeZone
|
||||
.getAvailableIDs()
|
||||
.mapIndexed { index, tz ->
|
||||
FavoriteZone(
|
||||
timeZone = TimeZone.getTimeZone(tz),
|
||||
position = index,
|
||||
label = if (tz == "ACT") "label text" else ""
|
||||
)
|
||||
}
|
||||
},
|
||||
customUserTime = null,
|
||||
userTimeZone = TimeZone.getTimeZone("Africa/Addis_Ababa"),
|
||||
selectedTimeZone = null,
|
||||
dialogState = TimeZoneDialogState.Nothing
|
||||
),
|
||||
navigateToMenu = {},
|
||||
openMenu = {},
|
||||
navigateToSettings = {},
|
||||
navigateToAddTimeZone = {},
|
||||
onDragEnd = { _, _ -> },
|
||||
onDelete = {},
|
||||
setSelectedTime = {},
|
||||
setCurrentTime = {},
|
||||
resetTime = {},
|
||||
setSelectedTime = {},
|
||||
onDragEnd = { _, _ -> },
|
||||
delete = {},
|
||||
updateLabel = { _, _ -> },
|
||||
selectTimeZone = {},
|
||||
setDialogState = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -18,11 +18,35 @@
|
||||
|
||||
package com.sadellie.unitto.feature.timezone
|
||||
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import android.icu.util.TimeZone
|
||||
import com.sadellie.unitto.data.model.timezone.FavoriteZone
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
data class TimeZoneUIState(
|
||||
val list: List<UnittoTimeZone> = emptyList(),
|
||||
val userTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
val updateTime: Boolean = true,
|
||||
)
|
||||
internal sealed class TimeZoneUIState {
|
||||
data object Loading : TimeZoneUIState()
|
||||
|
||||
data class Ready(
|
||||
val favorites: List<FavoriteZone>,
|
||||
val customUserTime: ZonedDateTime?,
|
||||
val userTimeZone: TimeZone,
|
||||
val selectedTimeZone: FavoriteZone?,
|
||||
val dialogState: TimeZoneDialogState,
|
||||
) : TimeZoneUIState()
|
||||
}
|
||||
|
||||
internal sealed class TimeZoneDialogState {
|
||||
data object Nothing : TimeZoneDialogState()
|
||||
|
||||
data class UserTimePicker(
|
||||
val time: ZonedDateTime,
|
||||
) : TimeZoneDialogState()
|
||||
|
||||
data class FavoriteTimePicker(
|
||||
val timeZone: FavoriteZone,
|
||||
val time: ZonedDateTime,
|
||||
) : TimeZoneDialogState()
|
||||
|
||||
data class LabelEditPicker(
|
||||
val timeZone: FavoriteZone,
|
||||
) : TimeZoneDialogState()
|
||||
}
|
||||
|
@ -18,61 +18,72 @@
|
||||
|
||||
package com.sadellie.unitto.feature.timezone
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import com.sadellie.unitto.data.common.stateIn
|
||||
import com.sadellie.unitto.data.model.timezone.FavoriteZone
|
||||
import com.sadellie.unitto.data.timezone.TimeZonesRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.ZonedDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@HiltViewModel
|
||||
class TimeZoneViewModel @Inject constructor(
|
||||
internal class TimeZoneViewModel @Inject constructor(
|
||||
private val timezonesRepository: TimeZonesRepository
|
||||
): ViewModel() {
|
||||
private val _userTimeZone = MutableStateFlow(TimeZone.getDefault())
|
||||
private val _customUserTime = MutableStateFlow<ZonedDateTime?>(null)
|
||||
private val _selectedTimeZone = MutableStateFlow<FavoriteZone?>(null)
|
||||
private val _dialogState = MutableStateFlow<TimeZoneDialogState>(TimeZoneDialogState.Nothing)
|
||||
|
||||
private val _userTime = MutableStateFlow(ZonedDateTime.now())
|
||||
private val _updateTime = MutableStateFlow(true)
|
||||
|
||||
val timeZoneUIState = combine(
|
||||
_userTime,
|
||||
_updateTime,
|
||||
timezonesRepository.favoriteTimeZones
|
||||
) { userTime, updateTime, favorites ->
|
||||
return@combine TimeZoneUIState(
|
||||
list = favorites,
|
||||
userTime = userTime,
|
||||
updateTime = updateTime
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope, SharingStarted.WhileSubscribed(5000), TimeZoneUIState()
|
||||
val uiState = combine(
|
||||
_customUserTime,
|
||||
_userTimeZone,
|
||||
_selectedTimeZone,
|
||||
_dialogState,
|
||||
timezonesRepository.favoriteTimeZones,
|
||||
) { customUserTime, userTimeZone, selectedTimeZone, dialogState, favoriteTimeZones ->
|
||||
return@combine TimeZoneUIState.Ready(
|
||||
favorites = favoriteTimeZones,
|
||||
customUserTime = customUserTime,
|
||||
userTimeZone = userTimeZone,
|
||||
selectedTimeZone = selectedTimeZone,
|
||||
dialogState = dialogState
|
||||
)
|
||||
}
|
||||
.stateIn(viewModelScope, TimeZoneUIState.Loading)
|
||||
|
||||
fun onDragEnd(from: String, to: String) = viewModelScope.launch {
|
||||
timezonesRepository.swapTimeZones(from, to)
|
||||
fun setCurrentTime() = _customUserTime.update { null }
|
||||
|
||||
fun setSelectedTime(time: ZonedDateTime) = _customUserTime.update { time }
|
||||
|
||||
fun setDialogState(state: TimeZoneDialogState) = _dialogState.update { state }
|
||||
|
||||
fun onDragEnd(
|
||||
tz: FavoriteZone,
|
||||
targetPosition: Int
|
||||
) = viewModelScope.launch {
|
||||
timezonesRepository.moveTimeZone(tz, targetPosition)
|
||||
}
|
||||
|
||||
fun onDelete(timeZone: UnittoTimeZone) = viewModelScope.launch {
|
||||
timezonesRepository.delete(timeZone)
|
||||
fun delete(timeZone: FavoriteZone) = viewModelScope.launch {
|
||||
timezonesRepository.removeFromFavorites(timeZone)
|
||||
}
|
||||
|
||||
fun setCustomTime(time: ZonedDateTime) = viewModelScope.launch(Dispatchers.Default) {
|
||||
_updateTime.update { false }
|
||||
_userTime.update { time }
|
||||
}
|
||||
fun selectTimeZone(timeZone: FavoriteZone?) = _selectedTimeZone.update { timeZone }
|
||||
|
||||
fun resetTime() = viewModelScope.launch(Dispatchers.Default) {
|
||||
_updateTime.update { true }
|
||||
}
|
||||
|
||||
fun setCurrentTime() = viewModelScope.launch(Dispatchers.Default) {
|
||||
_userTime.update { ZonedDateTime.now() }
|
||||
fun updateLabel(
|
||||
timeZone: FavoriteZone,
|
||||
label: String
|
||||
) = viewModelScope.launch {
|
||||
timezonesRepository.updateLabel(timeZone = timeZone, label = label)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,317 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.feature.timezone.components
|
||||
|
||||
import android.icu.util.TimeZone
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sadellie.unitto.core.base.R
|
||||
import com.sadellie.unitto.core.ui.datetime.formatLocal
|
||||
import com.sadellie.unitto.core.ui.datetime.formatOffset
|
||||
import com.sadellie.unitto.core.ui.theme.numberHeadlineMedium
|
||||
import com.sadellie.unitto.data.common.offset
|
||||
import com.sadellie.unitto.data.model.timezone.FavoriteZone
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Composable
|
||||
internal fun FavoriteTimeZoneItem(
|
||||
modifier: Modifier,
|
||||
item: FavoriteZone,
|
||||
fromTime: ZonedDateTime,
|
||||
isDragging: Boolean,
|
||||
expanded: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onLabelClick: () -> Unit,
|
||||
onPrimaryClick: (ZonedDateTime) -> Unit,
|
||||
) {
|
||||
var deleteAnimationRunning by remember { mutableStateOf(false) }
|
||||
val animatedAlpha by animateFloatAsState(
|
||||
label = "delete animation",
|
||||
targetValue = if (deleteAnimationRunning) 0f else 1f,
|
||||
finishedListener = { if (it == 0f) onDelete() }
|
||||
)
|
||||
|
||||
val offsetTime by remember(fromTime) { mutableStateOf(item.timeZone.offset(fromTime)) }
|
||||
val offsetTimeFormatted = offsetTime.formatOffset(fromTime)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.graphicsLayer(alpha = animatedAlpha)
|
||||
.then(modifier)
|
||||
.clickable(enabled = !isDragging) { onClick() }
|
||||
.padding(vertical = 16.dp, horizontal = 12.dp)
|
||||
) {
|
||||
TimeZoneLabel(
|
||||
label = item.label,
|
||||
expanded = expanded,
|
||||
onLabelClick = onLabelClick
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding()
|
||||
.heightIn(min = 56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = item.timeZone.displayName,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = offsetTimeFormatted != null,
|
||||
label = "Nullable offset"
|
||||
) {
|
||||
Text(
|
||||
text = offsetTimeFormatted ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = offsetTime.formatLocal(),
|
||||
label = "Time change",
|
||||
transitionSpec = {
|
||||
fadeIn() togetherWith fadeOut() using (SizeTransform(clip = false))
|
||||
}
|
||||
) { time ->
|
||||
// TODO Add AM PM as dots (apply to 12 and 24 hour systems)
|
||||
Text(
|
||||
text = time,
|
||||
style = MaterialTheme.typography.numberHeadlineMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column {
|
||||
TimeZoneOption(
|
||||
title = stringResource(R.string.select_time_label),
|
||||
icon = Icons.Outlined.Schedule,
|
||||
contentDescription = stringResource(R.string.select_time_label),
|
||||
onClick = { onPrimaryClick(offsetTime) }
|
||||
)
|
||||
TimeZoneOption(
|
||||
title = stringResource(R.string.delete_label),
|
||||
icon = Icons.Outlined.Delete,
|
||||
contentDescription = stringResource(R.string.delete_label),
|
||||
onClick = { deleteAnimationRunning = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimeZoneOption(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable { onClick() }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimeZoneLabel(
|
||||
label: String,
|
||||
expanded: Boolean,
|
||||
onLabelClick: () -> Unit,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = label.isBlank(),
|
||||
modifier = if (expanded) Modifier.clickable { onLabelClick() } else Modifier,
|
||||
) { blank ->
|
||||
if (blank) {
|
||||
AnimatedVisibility(expanded) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.add_label),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class FavoriteTimeZoneItemParameter(
|
||||
val expanded: Boolean,
|
||||
val tz: FavoriteZone,
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private class FavoriteTimeZoneItemParameterProvider :
|
||||
PreviewParameterProvider<FavoriteTimeZoneItemParameter> {
|
||||
override val values: Sequence<FavoriteTimeZoneItemParameter>
|
||||
get() = sequenceOf(
|
||||
FavoriteTimeZoneItemParameter(
|
||||
expanded = false,
|
||||
tz = FavoriteZone(
|
||||
timeZone = TimeZone.getDefault(),
|
||||
position = 1,
|
||||
label = ""
|
||||
),
|
||||
),
|
||||
FavoriteTimeZoneItemParameter(
|
||||
expanded = false,
|
||||
tz = FavoriteZone(
|
||||
timeZone = TimeZone.getDefault(),
|
||||
position = 1,
|
||||
label = "Some text"
|
||||
),
|
||||
),
|
||||
FavoriteTimeZoneItemParameter(
|
||||
expanded = true,
|
||||
tz = FavoriteZone(
|
||||
timeZone = TimeZone.getDefault(),
|
||||
position = 1,
|
||||
label = ""
|
||||
),
|
||||
),
|
||||
FavoriteTimeZoneItemParameter(
|
||||
expanded = true,
|
||||
tz = FavoriteZone(
|
||||
timeZone = TimeZone.getDefault(),
|
||||
position = 1,
|
||||
label = "Some text"
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Preview(showBackground = true, backgroundColor = 0xFFC1C9FF)
|
||||
@Composable
|
||||
private fun PreviewFavoriteTimeZones(
|
||||
@PreviewParameter(FavoriteTimeZoneItemParameterProvider::class) tz: FavoriteTimeZoneItemParameter,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(tz.expanded) }
|
||||
|
||||
FavoriteTimeZoneItem(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
item = tz.tz,
|
||||
fromTime = ZonedDateTime.parse(
|
||||
"2023-05-01T14:00+03:00[Africa/Addis_Ababa]",
|
||||
DateTimeFormatter.ISO_ZONED_DATE_TIME
|
||||
),
|
||||
expanded = expanded,
|
||||
onClick = { expanded = !expanded },
|
||||
onDelete = {},
|
||||
onPrimaryClick = {},
|
||||
onLabelClick = {},
|
||||
isDragging = false
|
||||
)
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.feature.timezone.components
|
||||
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sadellie.unitto.core.ui.datetime.formatLocal
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
fun SelectableTimeZone(
|
||||
modifier: Modifier = Modifier,
|
||||
timeZone: UnittoTimeZone,
|
||||
currentTime: ZonedDateTime?,
|
||||
) {
|
||||
val offsetTime: ZonedDateTime? by remember(currentTime) {
|
||||
mutableStateOf(currentTime?.let { timeZone.offsetFrom(it) })
|
||||
}
|
||||
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = {
|
||||
Text(text = timeZone.nameRes)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(text = timeZone.id)
|
||||
},
|
||||
trailingContent = {
|
||||
Text(text = offsetTime?.formatLocal() ?: "", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
|
||||
@Composable
|
||||
fun PreviewSelectableTimeZone() {
|
||||
SelectableTimeZone(
|
||||
timeZone = UnittoTimeZone(
|
||||
id = "text",
|
||||
nameRes = "Time zone"
|
||||
),
|
||||
modifier = Modifier.width(440.dp),
|
||||
currentTime = ZonedDateTime.now(),
|
||||
)
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.feature.timezone.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sadellie.unitto.core.ui.datetime.formatLocal
|
||||
import com.sadellie.unitto.core.ui.datetime.formatOffset
|
||||
import com.sadellie.unitto.data.model.UnittoTimeZone
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun TimeZoneListItem(
|
||||
modifier: Modifier,
|
||||
timeZone: UnittoTimeZone,
|
||||
currentTime: ZonedDateTime,
|
||||
onClick: () -> Unit = {},
|
||||
onDelete: (UnittoTimeZone) -> Unit = {},
|
||||
color: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
onSwipe: (UnittoTimeZone) -> Unit = {},
|
||||
draggableState: AnchoredDraggableState<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
|
||||
)
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.feature.timezone.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sadellie.unitto.core.ui.common.squashable
|
||||
import com.sadellie.unitto.core.ui.datetime.UnittoDateTimeFormatter
|
||||
import com.sadellie.unitto.core.ui.datetime.formatOnlyHours
|
||||
import com.sadellie.unitto.core.ui.datetime.formatOnlyMinutes
|
||||
import com.sadellie.unitto.core.ui.theme.numberBodyLarge
|
||||
import com.sadellie.unitto.core.ui.theme.numberDisplayLarge
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
internal fun UserTimeZone(
|
||||
modifier: Modifier,
|
||||
userTime: ZonedDateTime,
|
||||
onClick: () -> Unit,
|
||||
onResetClick: () -> Unit,
|
||||
showReset: Boolean,
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.squashable(
|
||||
onClick = onClick,
|
||||
onLongClick = onResetClick,
|
||||
cornerRadiusRange = 8.dp..32.dp,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.tertiaryContainer)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = userTime.format(UnittoDateTimeFormatter.zoneFormatPattern),
|
||||
style = MaterialTheme.typography.numberBodyLarge,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
SlidingText(text = userTime.formatOnlyHours())
|
||||
TimeSeparator()
|
||||
SlidingText(text = userTime.formatOnlyMinutes())
|
||||
}
|
||||
|
||||
Text(
|
||||
text = userTime.format(UnittoDateTimeFormatter.dayMonthYear),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showReset,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut(),
|
||||
) {
|
||||
IconButton(onResetClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.History,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SlidingText(
|
||||
text: String,
|
||||
style: TextStyle = MaterialTheme.typography.numberDisplayLarge
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = text,
|
||||
label = "user time change",
|
||||
transitionSpec = {
|
||||
slideInVertically { height -> height } + fadeIn() togetherWith
|
||||
slideOutVertically { height -> -height } + fadeOut() using
|
||||
SizeTransform()
|
||||
}
|
||||
) { target ->
|
||||
Text(
|
||||
text = target,
|
||||
style = style,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimeSeparator(
|
||||
text: String = ":",
|
||||
style: TextStyle = MaterialTheme.typography.numberDisplayLarge
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = style,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewUserTimeZone() {
|
||||
UserTimeZone(
|
||||
modifier = Modifier,
|
||||
userTime = ZonedDateTime.now(),
|
||||
onClick = {},
|
||||
onResetClick = {},
|
||||
showReset = true
|
||||
)
|
||||
}
|
@ -18,6 +18,7 @@
|
||||
|
||||
package com.sadellie.unitto.feature.timezone.navigation
|
||||
|
||||
import android.os.Build
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
@ -25,6 +26,7 @@ import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import com.sadellie.unitto.core.base.TopLevelDestinations
|
||||
import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen
|
||||
import com.sadellie.unitto.core.ui.unittoComposable
|
||||
import com.sadellie.unitto.core.ui.unittoNavigation
|
||||
import com.sadellie.unitto.feature.timezone.AddTimeZoneRoute
|
||||
@ -38,11 +40,11 @@ private const val ADD_TIME_ZONE_ROUTE = "ADD_TIME_ZONE_ROUTE"
|
||||
private const val USER_TIME_ARG = "USER_TIME_ARG"
|
||||
|
||||
private fun NavController.navigateToAddTimeZone(
|
||||
userTime: ZonedDateTime?
|
||||
userTime: ZonedDateTime
|
||||
) {
|
||||
val formattedTime = userTime
|
||||
?.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
|
||||
?.replace("/", "|") // this is so wrong
|
||||
.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
|
||||
.replace("/", "|") // this is so wrong
|
||||
|
||||
navigate("$ADD_TIME_ZONE_ROUTE/$formattedTime")
|
||||
}
|
||||
@ -60,8 +62,13 @@ fun NavGraphBuilder.timeZoneGraph(
|
||||
)
|
||||
) {
|
||||
unittoComposable(start) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
UnittoEmptyScreen()
|
||||
return@unittoComposable
|
||||
}
|
||||
|
||||
TimeZoneRoute(
|
||||
navigateToMenu = navigateToMenu,
|
||||
openMenu = navigateToMenu,
|
||||
navigateToSettings = navigateToSettings,
|
||||
navigateToAddTimeZone = navController::navigateToAddTimeZone
|
||||
)
|
||||
@ -77,10 +84,16 @@ fun NavGraphBuilder.timeZoneGraph(
|
||||
}
|
||||
)
|
||||
) { stackEntry ->
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
UnittoEmptyScreen()
|
||||
return@unittoComposable
|
||||
}
|
||||
|
||||
val userTime = stackEntry.arguments
|
||||
?.getString(USER_TIME_ARG)
|
||||
?.replace("|", "/") // war crime, don't look
|
||||
?.let { ZonedDateTime.parse(it, DateTimeFormatter.ISO_ZONED_DATE_TIME) }
|
||||
?: ZonedDateTime.now()
|
||||
|
||||
AddTimeZoneRoute(
|
||||
navigateUp = navController::navigateUp,
|
||||
|
Loading…
x
Reference in New Issue
Block a user