From 1b5b803f74c3f160f71fe5c3a923faf8028513a3 Mon Sep 17 00:00:00 2001 From: Marek Sagan Date: Mon, 27 Apr 2026 16:56:49 +0200 Subject: [PATCH] Timezone converter added --- assets/property_icons/timezone.svg | 2 + assets/property_icons/timezone_filled.svg | 2 + lib/data/default_order.dart | 42 +++++ lib/data/property_unit_maps.dart | 42 +++++ lib/models/conversions.dart | 20 ++- lib/models/order.dart | 11 +- lib/models/properties_list.dart | 4 + lib/models/time_zone_property.dart | 191 ++++++++++++++++++++++ lib/pages/conversion_page.dart | 3 +- lib/utils/utils.dart | 3 + packages/translations/lib/l10n/app_en.arb | 40 +++++ 11 files changed, 347 insertions(+), 13 deletions(-) create mode 100644 assets/property_icons/timezone.svg create mode 100644 assets/property_icons/timezone_filled.svg create mode 100644 lib/models/time_zone_property.dart diff --git a/assets/property_icons/timezone.svg b/assets/property_icons/timezone.svg new file mode 100644 index 00000000..e45b33bc --- /dev/null +++ b/assets/property_icons/timezone.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/property_icons/timezone_filled.svg b/assets/property_icons/timezone_filled.svg new file mode 100644 index 00000000..1495bced --- /dev/null +++ b/assets/property_icons/timezone_filled.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/lib/data/default_order.dart b/lib/data/default_order.dart index bfc6b09f..ee696f2c 100644 --- a/lib/data/default_order.dart +++ b/lib/data/default_order.dart @@ -1,3 +1,4 @@ +import 'package:converterpro/models/time_zone_property.dart'; import 'package:converterpro/utils/utils.dart'; import 'package:units_converter/units_converter.dart'; @@ -7,6 +8,7 @@ const defaultPropertiesOrder = [ PROPERTYX.volume, PROPERTYX.currencies, PROPERTYX.time, + PROPERTYX.timezone, PROPERTYX.temperature, PROPERTYX.speed, PROPERTYX.mass, @@ -313,4 +315,44 @@ const Map> defaultUnitsOrder = { TORQUE.poundForceInch, TORQUE.poundalMeter, ], + PROPERTYX.timezone: [ + TIMEZONE.utcMinus12, + TIMEZONE.utcMinus11, + TIMEZONE.utcMinus10, + TIMEZONE.utcMinus9_30, + TIMEZONE.utcMinus9, + TIMEZONE.utcMinus8, + TIMEZONE.utcMinus7, + TIMEZONE.utcMinus6, + TIMEZONE.utcMinus5, + TIMEZONE.utcMinus4, + TIMEZONE.utcMinus3_30, + TIMEZONE.utcMinus3, + TIMEZONE.utcMinus2, + TIMEZONE.utcMinus1, + TIMEZONE.utc0, + TIMEZONE.utcPlus1, + TIMEZONE.utcPlus2, + TIMEZONE.utcPlus3, + TIMEZONE.utcPlus3_30, + TIMEZONE.utcPlus4, + TIMEZONE.utcPlus4_30, + TIMEZONE.utcPlus5, + TIMEZONE.utcPlus5_30, + TIMEZONE.utcPlus5_45, + TIMEZONE.utcPlus6, + TIMEZONE.utcPlus6_30, + TIMEZONE.utcPlus7, + TIMEZONE.utcPlus8, + TIMEZONE.utcPlus8_45, + TIMEZONE.utcPlus9, + TIMEZONE.utcPlus9_30, + TIMEZONE.utcPlus10, + TIMEZONE.utcPlus10_30, + TIMEZONE.utcPlus11, + TIMEZONE.utcPlus12, + TIMEZONE.utcPlus12_45, + TIMEZONE.utcPlus13, + TIMEZONE.utcPlus14, + ], }; diff --git a/lib/data/property_unit_maps.dart b/lib/data/property_unit_maps.dart index a58eb69e..093a4367 100644 --- a/lib/data/property_unit_maps.dart +++ b/lib/data/property_unit_maps.dart @@ -1,3 +1,4 @@ +import 'package:converterpro/models/time_zone_property.dart'; import 'package:flutter/cupertino.dart'; import 'package:translations/app_localizations.dart'; import 'package:units_converter/units_converter.dart'; @@ -16,6 +17,7 @@ Map getPropertyUiMap(BuildContext context) { PROPERTYX.volume: l10n.volume, PROPERTYX.currencies: l10n.currencies, PROPERTYX.time: l10n.time, + PROPERTYX.timezone: l10n.timezone, PROPERTYX.temperature: l10n.temperature, PROPERTYX.speed: l10n.speed, PROPERTYX.mass: l10n.mass, @@ -337,5 +339,45 @@ Map> getUnitUiMap(BuildContext context) { TORQUE.poundForceInch: l10n.poundForceInch, TORQUE.poundalMeter: l10n.poundalMeter, }, + PROPERTYX.timezone: { + TIMEZONE.utcMinus12: l10n.utcMinus12, + TIMEZONE.utcMinus11: l10n.utcMinus11, + TIMEZONE.utcMinus10: l10n.utcMinus10, + TIMEZONE.utcMinus9_30: l10n.utcMinus9_30, + TIMEZONE.utcMinus9: l10n.utcMinus9, + TIMEZONE.utcMinus8: l10n.utcMinus8, + TIMEZONE.utcMinus7: l10n.utcMinus7, + TIMEZONE.utcMinus6: l10n.utcMinus6, + TIMEZONE.utcMinus5: l10n.utcMinus5, + TIMEZONE.utcMinus4: l10n.utcMinus4, + TIMEZONE.utcMinus3_30: l10n.utcMinus3_30, + TIMEZONE.utcMinus3: l10n.utcMinus3, + TIMEZONE.utcMinus2: l10n.utcMinus2, + TIMEZONE.utcMinus1: l10n.utcMinus1, + TIMEZONE.utc0: l10n.utc0, + TIMEZONE.utcPlus1: l10n.utcPlus1, + TIMEZONE.utcPlus2: l10n.utcPlus2, + TIMEZONE.utcPlus3: l10n.utcPlus3, + TIMEZONE.utcPlus3_30: l10n.utcPlus3_30, + TIMEZONE.utcPlus4: l10n.utcPlus4, + TIMEZONE.utcPlus4_30: l10n.utcPlus4_30, + TIMEZONE.utcPlus5: l10n.utcPlus5, + TIMEZONE.utcPlus5_30: l10n.utcPlus5_30, + TIMEZONE.utcPlus5_45: l10n.utcPlus5_45, + TIMEZONE.utcPlus6: l10n.utcPlus6, + TIMEZONE.utcPlus6_30: l10n.utcPlus6_30, + TIMEZONE.utcPlus7: l10n.utcPlus7, + TIMEZONE.utcPlus8: l10n.utcPlus8, + TIMEZONE.utcPlus8_45: l10n.utcPlus8_45, + TIMEZONE.utcPlus9: l10n.utcPlus9, + TIMEZONE.utcPlus9_30: l10n.utcPlus9_30, + TIMEZONE.utcPlus10: l10n.utcPlus10, + TIMEZONE.utcPlus10_30: l10n.utcPlus10_30, + TIMEZONE.utcPlus11: l10n.utcPlus11, + TIMEZONE.utcPlus12: l10n.utcPlus12, + TIMEZONE.utcPlus12_45: l10n.utcPlus12_45, + TIMEZONE.utcPlus13: l10n.utcPlus13, + TIMEZONE.utcPlus14: l10n.utcPlus14, + }, }; } diff --git a/lib/models/conversions.dart b/lib/models/conversions.dart index cfb54b92..1dd3e8db 100644 --- a/lib/models/conversions.dart +++ b/lib/models/conversions.dart @@ -1,5 +1,6 @@ import 'package:converterpro/models/order.dart'; import 'package:converterpro/models/properties_list.dart'; +import 'package:converterpro/models/time_zone_property.dart'; import 'package:converterpro/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -46,10 +47,13 @@ class ConversionsNotifier signed: false, ), NUMERAL_SYSTEMS.hexadecimal => TextInputType.text, - _ => const TextInputType.numberWithOptions( - decimal: true, - signed: false, - ), + _ => switch (propertyx) { + PROPERTYX.timezone => TextInputType.datetime, + _ => const TextInputType.numberWithOptions( + decimal: true, + signed: false, + ), + }, }, validator: switch (e) { TEMPERATURE.celsius || @@ -61,7 +65,10 @@ class ConversionsNotifier NUMERAL_SYSTEMS.octal => VALIDATOR.octal, NUMERAL_SYSTEMS.decimal => VALIDATOR.decimal, NUMERAL_SYSTEMS.hexadecimal => VALIDATOR.hexadecimal, - _ => VALIDATOR.rationalNonNegative, + _ => switch (propertyx) { + PROPERTYX.timezone => VALIDATOR.time, + _ => VALIDATOR.rationalNonNegative, + }, }, ), ) @@ -116,7 +123,8 @@ class ConversionsNotifier ///Clears the values of the current page Future clearAllValues(PROPERTYX property) async { List currentUnitDataList = state.value![property]!; - if (currentUnitDataList[0].property == PROPERTYX.numeralSystems) { + if (currentUnitDataList[0].property == PROPERTYX.numeralSystems || + currentUnitDataList[0].property == PROPERTYX.timezone) { _savedUnitDataList = [ ...currentUnitDataList.map((unitData) => unitData.unit.stringValue), ]; diff --git a/lib/models/order.dart b/lib/models/order.dart index 826ee5cb..3b4f9bed 100644 --- a/lib/models/order.dart +++ b/lib/models/order.dart @@ -101,7 +101,7 @@ class UnitsOrderNotifier extends AsyncNotifier> { if (storedList == null) { newState[property] = defaultOrder; } else { - final storedOrder = storedList + var storedOrder = storedList .map( (storedString) => defaultOrder.firstWhereOrNull( (unit) => storedString == unit.toString(), @@ -110,11 +110,10 @@ class UnitsOrderNotifier extends AsyncNotifier> { .nonNulls .cast() .toList(); - // Add missing units - if (storedOrder.length != defaultOrder.length) { - storedOrder.addAll( - defaultOrder.toSet().difference(storedOrder.toSet()), - ); + // Reset to default order when units are missing or out of order + if (storedOrder.length != defaultOrder.length || + !listEquals(storedOrder, defaultOrder)) { + storedOrder = defaultOrder.toList(); (await ref.read( sharedPref.future, )).setStringList(storeKey(property), _toStorableString(storedOrder)); diff --git a/lib/models/properties_list.dart b/lib/models/properties_list.dart index 75c8bdf2..ff233462 100644 --- a/lib/models/properties_list.dart +++ b/lib/models/properties_list.dart @@ -1,5 +1,6 @@ import 'package:converterpro/models/currencies.dart'; import 'package:converterpro/models/settings.dart'; +import 'package:converterpro/models/time_zone_property.dart'; import 'package:converterpro/utils/utils.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:units_converter/units_converter.dart'; @@ -151,5 +152,8 @@ final propertiesMapProvider = FutureProvider>(( removeTrailingZeros: removeTrailingZeros, name: PROPERTYX.torque, ), + PROPERTYX.timezone: TimeZoneProperty( + name: PROPERTYX.timezone, + ), }; }); diff --git a/lib/models/time_zone_property.dart b/lib/models/time_zone_property.dart new file mode 100644 index 00000000..5de68f48 --- /dev/null +++ b/lib/models/time_zone_property.dart @@ -0,0 +1,191 @@ +import 'package:units_converter/models/property.dart'; +import 'package:units_converter/models/unit.dart'; + +/// Consolidated timezone units grouped by UTC offset +enum TIMEZONE { + utcMinus12, + utcMinus11, + utcMinus10, + utcMinus9_30, + utcMinus9, + utcMinus8, + utcMinus7, + utcMinus6, + utcMinus5, + utcMinus4, + utcMinus3_30, + utcMinus3, + utcMinus2, + utcMinus1, + utc0, + utcPlus1, + utcPlus2, + utcPlus3, + utcPlus3_30, + utcPlus4, + utcPlus4_30, + utcPlus5, + utcPlus5_30, + utcPlus5_45, + utcPlus6, + utcPlus6_30, + utcPlus7, + utcPlus8, + utcPlus8_45, + utcPlus9, + utcPlus9_30, + utcPlus10, + utcPlus10_30, + utcPlus11, + utcPlus12, + utcPlus12_45, + utcPlus13, + utcPlus14, +} + +class TimeZoneProperty extends Property { + static const Map mapSymbols = { + TIMEZONE.utcMinus12: 'UTC-12', + TIMEZONE.utcMinus11: 'UTC-11', + TIMEZONE.utcMinus10: 'UTC-10', + TIMEZONE.utcMinus9_30: 'UTC-09:30', + TIMEZONE.utcMinus9: 'UTC-09', + TIMEZONE.utcMinus8: 'UTC-08', + TIMEZONE.utcMinus7: 'UTC-07', + TIMEZONE.utcMinus6: 'UTC-06', + TIMEZONE.utcMinus5: 'UTC-05', + TIMEZONE.utcMinus4: 'UTC-04', + TIMEZONE.utcMinus3_30: 'UTC-03:30', + TIMEZONE.utcMinus3: 'UTC-03', + TIMEZONE.utcMinus2: 'UTC-02', + TIMEZONE.utcMinus1: 'UTC-01', + TIMEZONE.utc0: 'UTC+0', + TIMEZONE.utcPlus1: 'UTC+01', + TIMEZONE.utcPlus2: 'UTC+02', + TIMEZONE.utcPlus3: 'UTC+03', + TIMEZONE.utcPlus3_30: 'UTC+03:30', + TIMEZONE.utcPlus4: 'UTC+04', + TIMEZONE.utcPlus4_30: 'UTC+04:30', + TIMEZONE.utcPlus5: 'UTC+05', + TIMEZONE.utcPlus5_30: 'UTC+05:30', + TIMEZONE.utcPlus5_45: 'UTC+05:45', + TIMEZONE.utcPlus6: 'UTC+06', + TIMEZONE.utcPlus6_30: 'UTC+06:30', + TIMEZONE.utcPlus7: 'UTC+07', + TIMEZONE.utcPlus8: 'UTC+08', + TIMEZONE.utcPlus8_45: 'UTC+08:45', + TIMEZONE.utcPlus9: 'UTC+09', + TIMEZONE.utcPlus9_30: 'UTC+09:30', + TIMEZONE.utcPlus10: 'UTC+10', + TIMEZONE.utcPlus10_30: 'UTC+10:30', + TIMEZONE.utcPlus11: 'UTC+11', + TIMEZONE.utcPlus12: 'UTC+12', + TIMEZONE.utcPlus12_45: 'UTC+12:45', + TIMEZONE.utcPlus13: 'UTC+13', + TIMEZONE.utcPlus14: 'UTC+14', + }; + + /// Offset from UTC in minutes + static const Map _offsetMinutes = { + TIMEZONE.utcMinus12: -720, + TIMEZONE.utcMinus11: -660, + TIMEZONE.utcMinus10: -600, + TIMEZONE.utcMinus9_30: -570, + TIMEZONE.utcMinus9: -540, + TIMEZONE.utcMinus8: -480, + TIMEZONE.utcMinus7: -420, + TIMEZONE.utcMinus6: -360, + TIMEZONE.utcMinus5: -300, + TIMEZONE.utcMinus4: -240, + TIMEZONE.utcMinus3_30: -210, + TIMEZONE.utcMinus3: -180, + TIMEZONE.utcMinus2: -120, + TIMEZONE.utcMinus1: -60, + TIMEZONE.utc0: 0, + TIMEZONE.utcPlus1: 60, + TIMEZONE.utcPlus2: 120, + TIMEZONE.utcPlus3: 180, + TIMEZONE.utcPlus3_30: 210, + TIMEZONE.utcPlus4: 240, + TIMEZONE.utcPlus4_30: 270, + TIMEZONE.utcPlus5: 300, + TIMEZONE.utcPlus5_30: 330, + TIMEZONE.utcPlus5_45: 345, + TIMEZONE.utcPlus6: 360, + TIMEZONE.utcPlus6_30: 390, + TIMEZONE.utcPlus7: 420, + TIMEZONE.utcPlus8: 480, + TIMEZONE.utcPlus8_45: 525, + TIMEZONE.utcPlus9: 540, + TIMEZONE.utcPlus9_30: 570, + TIMEZONE.utcPlus10: 600, + TIMEZONE.utcPlus10_30: 630, + TIMEZONE.utcPlus11: 660, + TIMEZONE.utcPlus12: 720, + TIMEZONE.utcPlus12_45: 765, + TIMEZONE.utcPlus13: 780, + TIMEZONE.utcPlus14: 840, + }; + + final List _unitList = []; + + TimeZoneProperty({dynamic name}) { + this.name = name; + size = TIMEZONE.values.length; + mapSymbols.forEach( + (key, value) => _unitList.add(Unit(key, symbol: value)), + ); + } + + @override + void convert(TIMEZONE name, String? value) { + if (value == null) { + for (Unit unit in _unitList) { + unit.value = null; + unit.stringValue = null; + } + return; + } + + final parts = value.split(':'); + if (parts.length != 2) return; + final hour = int.tryParse(parts[0]); + final minute = int.tryParse(parts[1]); + if (hour == null || + minute == null || + hour < 0 || + hour > 23 || + minute < 0 || + minute > 59) { + return; + } + + final sourceTotalMinutes = hour * 60 + minute; + final sourceOffset = _offsetMinutes[name]!; + + for (var unit in _unitList) { + if (unit.name == name) { + unit.stringValue = value; + } else { + final targetOffset = _offsetMinutes[unit.name as TIMEZONE]!; + var targetTotalMinutes = + sourceTotalMinutes + (targetOffset - sourceOffset); + targetTotalMinutes = targetTotalMinutes % (24 * 60); + if (targetTotalMinutes < 0) { + targetTotalMinutes += 24 * 60; + } + final targetHour = targetTotalMinutes ~/ 60; + final targetMinute = targetTotalMinutes % 60; + unit.stringValue = + '${targetHour.toString().padLeft(2, '0')}:${targetMinute.toString().padLeft(2, '0')}'; + } + } + } + + @override + List getAll() => _unitList; + + @override + Unit getUnit(var name) => + _unitList.where((element) => element.name == name).single; +} diff --git a/lib/pages/conversion_page.dart b/lib/pages/conversion_page.dart index 0b7444dc..b9fc904f 100644 --- a/lib/pages/conversion_page.dart +++ b/lib/pages/conversion_page.dart @@ -102,7 +102,8 @@ class ConversionPage extends ConsumerWidget { if (txt == '' || unitData.getValidator().hasMatch(txt)) { var conversions = ref.read(ConversionsNotifier.provider.notifier); //just numeral system uses a string for conversion - if (unitData.property == PROPERTYX.numeralSystems) { + if (unitData.property == PROPERTYX.numeralSystems || + unitData.property == PROPERTYX.timezone) { conversions.convert(unitData, txt == "" ? null : txt, property); } else { conversions.convert( diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 02513816..144ea006 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -37,6 +37,7 @@ enum VALIDATOR { hexadecimal, rational, rationalNonNegative, + time, } class UnitData { @@ -63,6 +64,7 @@ class UnitData { VALIDATOR.decimal => RegExp(r'^[0-9]+$'), VALIDATOR.hexadecimal => RegExp(r'^[0-9A-Fa-f]+$'), VALIDATOR.rational => RegExp(r'^([+-]?\d+)\.?(\d*)(e[+-]?\d+)?$'), + VALIDATOR.time => RegExp(r'^([01]?[0-9]|2[0-3]):[0-5][0-9]$'), _ => RegExp(r'^(\+?\d+)\.?(\d*)(e[+-]?\d+)?$'), }; } @@ -88,6 +90,7 @@ enum PROPERTYX { speed, temperature, time, + timezone, torque, volume, } diff --git a/packages/translations/lib/l10n/app_en.arb b/packages/translations/lib/l10n/app_en.arb index 10fae51f..eb5c249b 100644 --- a/packages/translations/lib/l10n/app_en.arb +++ b/packages/translations/lib/l10n/app_en.arb @@ -110,6 +110,7 @@ "force": "Force", "fuelConsumption": "Fuel consumption", "numeralSystems": "Numeral systems", + "timezone": "Time zones", "meters": "Meters", "centimeters": "Centimeters", @@ -201,6 +202,45 @@ "centuries": "Centuries", "millennium": "Millennium", + "utcMinus12": "Baker Island (UTC-12)", + "utcMinus11": "American Samoa (UTC-11)", + "utcMinus10": "Hawaii (UTC-10)", + "utcMinus9_30": "Marquesas Islands (UTC-09:30)", + "utcMinus9": "Alaska (UTC-09)", + "utcMinus8": "Pacific Time (UTC-08)", + "utcMinus7": "Mountain Time (UTC-07)", + "utcMinus6": "Central Time (UTC-06)", + "utcMinus5": "Eastern Time (UTC-05)", + "utcMinus4": "Atlantic Time (UTC-04)", + "utcMinus3_30": "Newfoundland (UTC-03:30)", + "utcMinus3": "Argentina (UTC-03)", + "utcMinus2": "South Georgia (UTC-02)", + "utcMinus1": "Azores (UTC-01)", + "utc0": "United Kingdom (UTC+0)", + "utcPlus1": "Central Europe (UTC+01)", + "utcPlus2": "Eastern Europe (UTC+02)", + "utcPlus3": "Moscow (UTC+03)", + "utcPlus3_30": "Iran (UTC+03:30)", + "utcPlus4": "UAE (UTC+04)", + "utcPlus4_30": "Afghanistan (UTC+04:30)", + "utcPlus5": "Pakistan (UTC+05)", + "utcPlus5_30": "India (UTC+05:30)", + "utcPlus5_45": "Nepal (UTC+05:45)", + "utcPlus6": "Bangladesh (UTC+06)", + "utcPlus6_30": "Myanmar (UTC+06:30)", + "utcPlus7": "Thailand (UTC+07)", + "utcPlus8": "China (UTC+08)", + "utcPlus8_45": "Eucla (UTC+08:45)", + "utcPlus9": "Japan (UTC+09)", + "utcPlus9_30": "Central Australia (UTC+09:30)", + "utcPlus10": "Eastern Australia (UTC+10)", + "utcPlus10_30": "Lord Howe Island (UTC+10:30)", + "utcPlus11": "Solomon Islands (UTC+11)", + "utcPlus12": "New Zealand (UTC+12)", + "utcPlus12_45": "Chatham Islands (UTC+12:45)", + "utcPlus13": "Tonga (UTC+13)", + "utcPlus14": "Line Islands (UTC+14)", + "fahrenheit": "Fahrenheit", "celsius": "Celsius (Centigrade)", "kelvin": "Kelvin",