diff --git a/CHANGELOG.md b/CHANGELOG.md index a45844e..fb011d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Changelog +## 7.5.16 + +- Native support for `TZDateTime` on Date and DateTime pickers. + ## 7.5.15 + - Adjusted `ThemedDateTimeSteppedPicker` to call the onChanged method only once after the time is selected. ## 7.5.14 + - Created `ThemedDateTimeSteppedPicker` widget for a stepped date and time picking experience, allowing users to select date first and then time ## 7.5.13 diff --git a/example/lib/main.dart b/example/lib/main.dart index a1f57e5..e0d5539 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,13 +6,15 @@ import 'package:layrz_theme_example/router.dart'; import 'package:layrz_theme_example/store/store.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:layrz_state/layrz_state.dart'; +import 'package:layrz_theme_example/timezone/native.dart' + if (dart.library.js_interop) 'package:layrz_theme_example/timezone/web.dart'; const font = AppFont(source: .google, name: 'Ubuntu Mono'); Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await ThemedFontHandler.preloadFont(font); + await initializeTimeZone(); final prefs = await SharedPreferences.getInstance(); final rawThemeMode = prefs.getString('layrz.theme.mode'); diff --git a/example/lib/router.dart b/example/lib/router.dart index 49018e6..312acfc 100644 --- a/example/lib/router.dart +++ b/example/lib/router.dart @@ -145,7 +145,7 @@ final goRoutes = [ ]; final router = GoRouter( - initialLocation: kDebugMode ? '/map/layer' : '/', + initialLocation: kDebugMode ? '/inputs/selectors/datetime' : '/', errorPageBuilder: (context, state) => customTransitionBuilder(context, state, const NotFoundView()), routes: goRoutes, ); diff --git a/example/lib/timezone/native.dart b/example/lib/timezone/native.dart new file mode 100644 index 0000000..6ca0815 --- /dev/null +++ b/example/lib/timezone/native.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:timezone/standalone.dart' as tz; +import 'package:timezone/data/latest_all.dart' as tzl; +import 'package:flutter_timezone/flutter_timezone.dart'; + +export 'package:timezone/standalone.dart' show Location; + +/// [initializeTimeZone] is a stub that should be implemented in platform-specific code to initialize the timezone data. +Future initializeTimeZone() async { + debugPrint('layrz_utils/timezone: Initializing timezone data'); + try { + await tz.initializeTimeZone('https://cdn.layrz.com/resources/utils/timezones-2025.tzf'); + } catch (err) { + debugPrint( + 'layrz_utils/timezone: Error initializing timezone data from Layrz CDN, falling back to built-in data', + ); + tzl.initializeTimeZones(); + } + return Future.value(); +} + +/// [getTimezone] is a stub that should be implemented in platform-specific code to get the current timezone. +Future getTimezone() async { + debugPrint('layrz_utils/timezone: Getting current timezone'); + final tzinfo = await FlutterTimezone.getLocalTimezone(); + return tz.getLocation(tzinfo.identifier); +} + +/// [setTimezone] is a stub that should be implemented in platform-specific code to set the timezone. +Future setTimezone(tz.Location timezone) { + final pre = DateTime.now().toIso8601String(); + tz.setLocalLocation(timezone); + final post = tz.TZDateTime.now(timezone).toIso8601String(); + debugPrint('layrz_utils/timezone: Timezone changed from $pre to $post'); + return Future.value(); +} + +/// [castTimezone] converts a timezone string to a [Location] object. If the input is null or invalid, +/// it defaults to the device's timezone. +Future castTimezone(String? timezone) async { + if (timezone == null) return await getTimezone(); + try { + return tz.getLocation(timezone); + } catch (e) { + debugPrint('layrz_utils/timezone: Invalid timezone "$timezone", defaulting to browser timezone'); + return await getTimezone(); + } +} + +/// [getTimezones] gets the list of available timezones +List getTimezones() => tz.timeZoneDatabase.locations.values.toList(); diff --git a/example/lib/timezone/web.dart b/example/lib/timezone/web.dart new file mode 100644 index 0000000..629050c --- /dev/null +++ b/example/lib/timezone/web.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:timezone/browser.dart' as tz; +import 'package:timezone/data/latest_all.dart' as tzl; +import 'package:flutter_timezone/flutter_timezone.dart'; + +export 'package:timezone/browser.dart' show Location; + +/// [initializeTimeZone] is a stub that should be implemented in platform-specific code to initialize the timezone data. +Future initializeTimeZone() async { + debugPrint('layrz_utils/timezone: Initializing timezone data'); + try { + await tz.initializeTimeZone('https://cdn.layrz.com/resources/utils/timezones-2025.tzf'); + } catch (err) { + debugPrint( + 'layrz_utils/timezone: Error initializing timezone data from Layrz CDN, falling back to built-in data', + ); + tzl.initializeTimeZones(); + } + return Future.value(); +} + +/// [getTimezone] is a stub that should be implemented in platform-specific code to get the current timezone. +Future getTimezone() async { + debugPrint('layrz_utils/timezone: Getting current timezone'); + final tzinfo = await FlutterTimezone.getLocalTimezone(); + return tz.getLocation(tzinfo.identifier); +} + +/// [setTimezone] is a stub that should be implemented in platform-specific code to set the timezone. +Future setTimezone(tz.Location timezone) { + final pre = DateTime.now().toIso8601String(); + tz.setLocalLocation(timezone); + final post = tz.TZDateTime.now(timezone).toIso8601String(); + debugPrint('layrz_utils/timezone: Timezone changed from $pre to $post'); + return Future.value(); +} + +/// [castTimezone] converts a timezone string to a [Location] object. If the input is null or invalid, +/// it defaults to the device's timezone. +Future castTimezone(String? timezone) async { + if (timezone == null) return await getTimezone(); + try { + return tz.getLocation(timezone); + } catch (e) { + debugPrint('layrz_utils/timezone: Invalid timezone "$timezone", defaulting to browser timezone'); + return await getTimezone(); + } +} + +/// [getTimezones] gets the list of available timezones +List getTimezones() => tz.timeZoneDatabase.locations.values.toList(); diff --git a/example/lib/views/inputs/inputs.dart b/example/lib/views/inputs/inputs.dart index 8470db2..a4f3bfb 100644 --- a/example/lib/views/inputs/inputs.dart +++ b/example/lib/views/inputs/inputs.dart @@ -1,10 +1,16 @@ library; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:layrz_icons/layrz_icons.dart'; import 'package:layrz_theme/layrz_theme.dart'; import 'package:layrz_theme_example/store/store.dart'; +import 'package:timezone/standalone.dart'; +import 'package:timezone/timezone.dart'; + +import 'package:layrz_theme_example/timezone/native.dart' + if (dart.library.js_interop) 'package:layrz_theme_example/timezone/web.dart'; part 'src/buttons.dart'; part 'src/calendar.dart'; diff --git a/example/lib/views/inputs/src/buttons.dart b/example/lib/views/inputs/src/buttons.dart index 9e978f4..5e047bc 100644 --- a/example/lib/views/inputs/src/buttons.dart +++ b/example/lib/views/inputs/src/buttons.dart @@ -466,7 +466,7 @@ class _ButtonsViewState extends State { } Widget _factorButton({ - required style, + required ThemedButtonStyle style, ThemedTooltipPosition tooltipPosition = ThemedTooltipPosition.right, }) { return ThemedButton( diff --git a/example/lib/views/inputs/src/selectors/datetime.dart b/example/lib/views/inputs/src/selectors/datetime.dart index d15bf66..fa73af5 100644 --- a/example/lib/views/inputs/src/selectors/datetime.dart +++ b/example/lib/views/inputs/src/selectors/datetime.dart @@ -20,6 +20,14 @@ class _DateTimePickersViewState extends State { ThemedMonth? _selectedMonth; List _selectedMonthRange = []; + late Location tz; + @override + void initState() { + super.initState(); + tz = getLocation('Asia/Tokyo'); + setTimezone(tz); + } + @override Widget build(BuildContext context) { return Layout( @@ -43,10 +51,20 @@ class _DateTimePickersViewState extends State { ), const SizedBox(height: 10), const Text("Classic picker"), + ThemedDatePicker( labelText: "Example label", value: _selectedDate, - onChanged: (val) => setState(() => _selectedDate = val), + onChanged: (val) { + setState(() => _selectedDate = val); + if (val is TZDateTime) { + debugPrint('Selected date: $val (TZDateTime) in timezone ${tz.name}'); + _selectedDate = val; + } else { + _selectedDate = TZDateTime.from(val, tz); + debugPrint('Selected date: $_selectedDate (converted to TZDateTime) in timezone ${tz.name}'); + } + }, ), const Text("And the range variant"), ThemedDateRangePicker( @@ -99,16 +117,54 @@ class _DateTimePickersViewState extends State { ), const SizedBox(height: 10), const Text("Classic picker"), + if (kDebugMode) ...[ + if (_selectedDateTime is TZDateTime) + Text('Selected date and time: $_selectedDateTime (TZDateTime) in timezone ${tz.name}') + else if (_selectedDateTime != null) + Text('Selected date and time: $_selectedDateTime (DateTime, not converted to TZDateTime)'), + ], ThemedDateTimePicker( labelText: "Example label", value: _selectedDateTime, - onChanged: (val) => setState(() => _selectedDateTime = val), + onChanged: (val) { + debugPrint("Raw selected date and time: $val (${val.runtimeType})"); + if (val is TZDateTime) { + debugPrint('Selected date and time: $val (TZDateTime) in timezone ${tz.name}'); + setState(() => _selectedDateTime = val); + } else { + final converted = TZDateTime.from(val, tz); + debugPrint('Selected date and time: $converted (converted to TZDateTime) in timezone ${tz.name}'); + setState(() => _selectedDateTime = converted); + } + }, ), const Text("And the range variant"), + if (kDebugMode) ...[ + for (final entry in _selectedDateTimeRange.asMap().entries) ...[ + if (entry.value is TZDateTime) ...[ + Text('[${entry.key}] Selected date and time: ${entry.value} (TZDateTime) in timezone ${tz.name}'), + ] else ...[ + Text('[${entry.key}] Selected date and time: ${entry.value} (DateTime, not converted to TZDateTime)'), + ], + ], + ], ThemedDateTimeRangePicker( labelText: "Example label", value: _selectedDateTimeRange, - onChanged: (val) => setState(() => _selectedDateTimeRange = val), + onChanged: (val) { + debugPrint("Raw selected date and time range: $val"); + final convertedRange = val.map((dateTime) { + if (dateTime is TZDateTime) { + debugPrint('Selected date and time: $dateTime (TZDateTime) in timezone ${tz.name}'); + return dateTime; + } else { + final converted = TZDateTime.from(dateTime, tz); + debugPrint('Selected date and time: $converted (converted to TZDateTime) in timezone ${tz.name}'); + return converted; + } + }).toList(); + setState(() => _selectedDateTimeRange = convertedRange); + }, ), const Text("Stepped variant, after selecting the date, you will select the time"), ThemedDateTimeSteppedPicker( diff --git a/example/lib/views/map/src/layer.dart b/example/lib/views/map/src/layer.dart index db61181..ce6df85 100644 --- a/example/lib/views/map/src/layer.dart +++ b/example/lib/views/map/src/layer.dart @@ -35,7 +35,7 @@ class _MapLayerViewState extends State with TickerProviderStateMix super.dispose(); } - void _listener(event) { + void _listener(ThemedMapEvent event) { debugPrint('layrz_theme_example/ThemedMapController() event: $event'); } diff --git a/example/pubspec.lock b/example/pubspec.lock index 8833a7b..1295f70 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.9" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -202,10 +210,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_map: dependency: "direct main" description: @@ -243,6 +251,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timezone: + dependency: "direct main" + description: + name: flutter_timezone + sha256: "978192f2f9ea6d019a4de4f0211d76a9af955ca24865828fa98ca4e20cf0cb3c" + url: "https://pub.dev" + source: hosted + version: "5.0.1" flutter_web_plugins: dependency: transitive description: flutter @@ -374,7 +390,7 @@ packages: path: ".." relative: true source: path - version: "7.5.14" + version: "7.5.16" leak_tracker: dependency: transitive description: @@ -411,10 +427,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.1.0" lists: dependency: transitive description: @@ -804,6 +820,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.9" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" two_dimensional_scrollables: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2c5e499..009aafe 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -22,6 +22,8 @@ dependencies: layrz_state: any layrz_icons: any layrz_models: any + timezone: any + flutter_timezone: any layrz_theme: path: ../ @@ -29,7 +31,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/lib/src/inputs/inputs.dart b/lib/src/inputs/inputs.dart index 60badbf..37dd8be 100644 --- a/lib/src/inputs/inputs.dart +++ b/lib/src/inputs/inputs.dart @@ -36,6 +36,7 @@ import 'package:layrz_theme/src/languages/lml/lml.dart' as lml; import 'package:layrz_theme/src/languages/python/python.dart' as python; import 'package:layrz_theme/src/languages/mjml/mjml.dart' as mjml; import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:timezone/standalone.dart'; import 'package:url_launcher/url_launcher_string.dart'; export 'package:emojis/emoji.dart' show Emoji, EmojiGroup; @@ -90,7 +91,6 @@ part 'src/pickers/datetime/single.dart'; part 'src/pickers/datetime/range.dart'; part 'src/pickers/datetime/single_stepped.dart'; - // Blocks part 'src/dynamic_configurable/block.dart'; part 'src/dynamic_configurable/dialog.dart'; diff --git a/lib/src/inputs/src/general/multiselect_input.dart b/lib/src/inputs/src/general/multiselect_input.dart index abca97d..66cc77e 100644 --- a/lib/src/inputs/src/general/multiselect_input.dart +++ b/lib/src/inputs/src/general/multiselect_input.dart @@ -186,6 +186,7 @@ class _ThemedMultiSelectInputState extends State> w } void _handleUpdate({bool force = false, List previousValues = const [], List newValues = const []}) { + debugPrint("_handleUpdate: ${previousValues.length} vs ${newValues.length}"); if (newValues.isEmpty && force) { if (widget.autoselectFirst && widget.items.isNotEmpty) { selected = [widget.items.first]; diff --git a/lib/src/inputs/src/pickers/date/range.dart b/lib/src/inputs/src/pickers/date/range.dart index 386bb7b..1daa42c 100644 --- a/lib/src/inputs/src/pickers/date/range.dart +++ b/lib/src/inputs/src/pickers/date/range.dart @@ -241,6 +241,12 @@ class _ThemedDateRangePickerState extends State { ); if (selected != null) { + if (widget.value.isNotEmpty) { + if (widget.value.first is TZDateTime) { + final tz = (widget.value.first as TZDateTime).location; + selected = selected.map((e) => TZDateTime(tz, e.year, e.month, e.day)).toList(); + } + } widget.onChanged?.call(selected); } } diff --git a/lib/src/inputs/src/pickers/date/single.dart b/lib/src/inputs/src/pickers/date/single.dart index 849c39c..1804acc 100644 --- a/lib/src/inputs/src/pickers/date/single.dart +++ b/lib/src/inputs/src/pickers/date/single.dart @@ -209,6 +209,10 @@ class _ThemedDatePickerState extends State { ); if (selected != null) { + if (widget.value is TZDateTime) { + final tz = (widget.value as TZDateTime).location; + selected = TZDateTime(tz, selected.year, selected.month, selected.day); + } widget.onChanged?.call(selected); } } diff --git a/lib/src/inputs/src/pickers/datetime/range.dart b/lib/src/inputs/src/pickers/datetime/range.dart index 4c8cd75..2a0360d 100644 --- a/lib/src/inputs/src/pickers/datetime/range.dart +++ b/lib/src/inputs/src/pickers/datetime/range.dart @@ -455,20 +455,48 @@ class _ThemedDateTimeRangeDialogState extends State w ThemedButton.save( labelText: t('actions.save'), onTap: () { - DateTime start = DateTime( - startDate.year, - startDate.month, - startDate.day, - startTime.hour, - startTime.minute, - ); - DateTime end = DateTime( - endDate.year, - endDate.month, - endDate.day, - endTime.hour, - endTime.minute, - ); + Location? tz; + if (widget.value.isNotEmpty && widget.value.first is TZDateTime) { + final value = widget.value.first; + if (value is TZDateTime) tz = value.location; + } + + DateTime start; + DateTime end; + + if (tz != null) { + start = TZDateTime( + tz, + startDate.year, + startDate.month, + startDate.day, + startTime.hour, + startTime.minute, + ); + end = TZDateTime( + tz, + endDate.year, + endDate.month, + endDate.day, + endTime.hour, + endTime.minute, + ); + } else { + start = DateTime( + startDate.year, + startDate.month, + startDate.day, + startTime.hour, + startTime.minute, + ); + end = DateTime( + endDate.year, + endDate.month, + endDate.day, + endTime.hour, + endTime.minute, + ); + } _tabController.animateTo(0); diff --git a/lib/src/inputs/src/pickers/datetime/single.dart b/lib/src/inputs/src/pickers/datetime/single.dart index 0bc3ffb..a8573ff 100644 --- a/lib/src/inputs/src/pickers/datetime/single.dart +++ b/lib/src/inputs/src/pickers/datetime/single.dart @@ -339,6 +339,24 @@ class _ThemedDateTimePickerState extends State with Single onTap: () { if (date != null && time != null) { _tabController.animateTo(0); + if (widget.value != null) { + final value = widget.value; + if (value is TZDateTime) { + Navigator.of(context).pop( + TZDateTime( + value.location, + date!.year, + date!.month, + date!.day, + time!.hour, + time!.minute, + 0, + ), + ); + return; + } + } + Navigator.of(context).pop( DateTime( date!.year, diff --git a/pubspec.yaml b/pubspec.yaml index 8c153ee..26112eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: layrz_theme description: Layrz standard styling library for Flutter. Widget library following the Material Design 3 guidelines, with a focus on reliavility and functionality. -version: "7.5.15" +version: "7.5.16" homepage: https://theme.layrz.com repository: https://github.com/goldenm-software/layrz_theme @@ -49,6 +49,8 @@ dependencies: layrz_icons: ^1.0.5 flutter_map_animations: ^0.9.0 layrz_state: ^1.0.1+1 + timezone: ^0.10.1 + flutter_timezone: ^5.0.0 dev_dependencies: flutter_test: