diff --git a/base_project/lib/core/providers/timezone_provider.dart b/base_project/lib/core/providers/timezone_provider.dart new file mode 100644 index 0000000..789df0f --- /dev/null +++ b/base_project/lib/core/providers/timezone_provider.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:timezone/timezone.dart' as tz; + +/// Single source of truth for time-zone display prefs (local-only v1). +/// Mirrors core LocaleProvider: ChangeNotifier + SharedPreferences keys +/// under the `app_*` convention + notifyListeners. +/// Backend sync (AppUser.multitime) is a later phase. +class TimezoneProvider with ChangeNotifier { + static const _zoneKey = 'app_timezone_id'; + static const _patternKey = 'app_date_pattern'; + static const _is24hKey = 'app_time_24h'; + static const _weekStartKey = 'app_week_start'; + + /// Curated quick-pick list (same as sibling app). + static const commonZoneIds = [ + 'Asia/Kolkata', + 'Asia/Dubai', + 'Asia/Singapore', + 'Asia/Kuala_Lumpur', + 'Asia/Hong_Kong', + 'Asia/Shanghai', + 'Asia/Taipei', + 'Asia/Tokyo', + 'Asia/Seoul', + 'Asia/Bangkok', + 'Asia/Jakarta', + 'Asia/Manila', + 'Asia/Karachi', + 'Europe/London', + 'Europe/Paris', + 'Europe/Berlin', + 'America/New_York', + 'America/Chicago', + 'America/Denver', + 'America/Los_Angeles', + 'America/Toronto', + 'Australia/Sydney', + 'UTC', + ]; + + static const datePatterns = ['dd-MM-yyyy', 'MM-dd-yyyy', 'yyyy-MM-dd']; + static const weekDays = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]; + + String _zoneId = 'Asia/Kolkata'; + String get zoneId => _zoneId; + + String _datePattern = 'dd-MM-yyyy'; + String get datePattern => _datePattern; + + bool _is24h = true; + bool get is24h => _is24h; + + String _weekStart = 'Monday'; + String get weekStart => _weekStart; + + bool _ready = false; + bool get ready => _ready; + + /// Short city label: 'Asia/Kolkata' -> 'Kolkata', 'UTC' -> 'UTC'. + static String cityOf(String id) { + if (id == 'UTC') return 'UTC'; + final parts = id.split('/'); + final raw = parts.isNotEmpty ? parts.last : id; + return raw.replaceAll('_', ' '); + } + + Future initialize() async { + final prefs = await SharedPreferences.getInstance(); + final z = prefs.getString(_zoneKey); + if (z != null && z.isNotEmpty) _zoneId = z; + final p = prefs.getString(_patternKey); + if (p != null && datePatterns.contains(p)) _datePattern = p; + final h = prefs.getBool(_is24hKey); + if (h != null) _is24h = h; + final w = prefs.getString(_weekStartKey); + if (w != null && weekDays.contains(w)) _weekStart = w; + _ready = true; + notifyListeners(); + } + + Future setZone(String id) async { + _zoneId = id; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_zoneKey, id); + notifyListeners(); + } + + Future setDatePattern(String p) async { + if (!datePatterns.contains(p)) return; + _datePattern = p; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_patternKey, p); + notifyListeners(); + } + + Future setIs24h(bool v) async { + _is24h = v; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_is24hKey, v); + notifyListeners(); + } + + Future setWeekStart(String d) async { + if (!weekDays.contains(d)) return; + _weekStart = d; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_weekStartKey, d); + notifyListeners(); + } + + /// Explicit save (card button). Values persist on change already; + /// this re-persists + notifies so callers can show a snackbar. + Future save() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_zoneKey, _zoneId); + await prefs.setString(_patternKey, _datePattern); + await prefs.setBool(_is24hKey, _is24h); + await prefs.setString(_weekStartKey, _weekStart); + notifyListeners(); + } + + tz.Location get location { + try { + return tz.getLocation(_zoneId); + } catch (_) { + return tz.getLocation('UTC'); + } + } + + tz.TZDateTime nowInZone() { + try { + return tz.TZDateTime.now(location); + } catch (_) { + return tz.TZDateTime.now(tz.getLocation('UTC')); + } + } + + /// e.g. 'GMT+5:30', 'GMT-4:00', 'GMT'. DST-aware via timezone DB. + String offsetLabel([tz.TZDateTime? at]) { + try { + final dt = at ?? nowInZone(); + final offset = dt.timeZoneOffset; + if (offset == Duration.zero) return 'GMT'; + final sign = offset.isNegative ? '-' : '+'; + final abs = offset.abs(); + final h = abs.inHours.toString(); + final m = (abs.inMinutes % 60).toString().padLeft(2, '0'); + return 'GMT$sign$h:$m'; + } catch (_) { + return ''; + } + } + + /// 'Kolkata (GMT+5:30)' style label for dropdown rows. + String displayLabel(String id) { + try { + final loc = tz.getLocation(id); + final now = tz.TZDateTime.now(loc); + final offset = now.timeZoneOffset; + final sign = offset.isNegative ? '-' : '+'; + final abs = offset.abs(); + final off = offset == Duration.zero + ? 'GMT' + : 'GMT$sign${abs.inHours}:${(abs.inMinutes % 60).toString().padLeft(2, '0')}'; + return '${cityOf(id)} ($off)'; + } catch (_) { + return cityOf(id); + } + } +} diff --git a/base_project/lib/l10n/app_en.arb b/base_project/lib/l10n/app_en.arb index 0b4fada..64ba1c9 100644 --- a/base_project/lib/l10n/app_en.arb +++ b/base_project/lib/l10n/app_en.arb @@ -174,4 +174,17 @@ ,"ohSnap": "Oh Snap!" ,"alertTitle": "Alert" ,"successTitle": "Success" + ,"timeZoneSettings": "Time Zone Settings" + ,"timeZoneSubtitle": "Set your local time, date & week preferences" + ,"livePreview": "LIVE PREVIEW" + ,"currentTimeZone": "CURRENT TIME ZONE" + ,"commonTimeZones": "COMMON TIME ZONES" + ,"dateFormat": "DATE FORMAT" + ,"timeFormat": "TIME FORMAT" + ,"hour12": "12 Hour" + ,"hour24": "24 Hour" + ,"firstDayOfWeek": "FIRST DAY OF WEEK" + ,"storedInUtcNote": "Stored in UTC - converted to your time zone on display" + ,"savePreferences": "Save Preferences" + ,"preferencesSaved": "Preferences saved" } diff --git a/base_project/lib/l10n/app_hi.arb b/base_project/lib/l10n/app_hi.arb index e921c84..fd7e79a 100644 --- a/base_project/lib/l10n/app_hi.arb +++ b/base_project/lib/l10n/app_hi.arb @@ -173,4 +173,17 @@ ,"ohSnap": "ओह स्नैप!" ,"alertTitle": "चेतावनी" ,"successTitle": "सफलता" + ,"timeZoneSettings": "समय क्षेत्र सेटिंग" + ,"timeZoneSubtitle": "अपना स्थानीय समय, तिथि और सप्ताह वरीयताएँ सेट करें" + ,"livePreview": "लाइव पूर्वावलोकन" + ,"currentTimeZone": "वर्तमान समय क्षेत्र" + ,"commonTimeZones": "सामान्य समय क्षेत्र" + ,"dateFormat": "तिथि प्रारूप" + ,"timeFormat": "समय प्रारूप" + ,"hour12": "12 घंटे" + ,"hour24": "24 घंटे" + ,"firstDayOfWeek": "सप्ताह का पहला दिन" + ,"storedInUtcNote": "UTC में संग्रहीत - प्रदर्शन पर आपके समय क्षेत्र में बदला गया" + ,"savePreferences": "वरीयताएँ सहेजें" + ,"preferencesSaved": "वरीयताएँ सहेजी गईं" } diff --git a/base_project/lib/l10n/app_localizations.dart b/base_project/lib/l10n/app_localizations.dart index 7a8a063..a7d6519 100644 --- a/base_project/lib/l10n/app_localizations.dart +++ b/base_project/lib/l10n/app_localizations.dart @@ -1075,6 +1075,84 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Success'** String get successTitle; + + /// No description provided for @timeZoneSettings. + /// + /// In en, this message translates to: + /// **'Time Zone Settings'** + String get timeZoneSettings; + + /// No description provided for @timeZoneSubtitle. + /// + /// In en, this message translates to: + /// **'Set your local time, date & week preferences'** + String get timeZoneSubtitle; + + /// No description provided for @livePreview. + /// + /// In en, this message translates to: + /// **'LIVE PREVIEW'** + String get livePreview; + + /// No description provided for @currentTimeZone. + /// + /// In en, this message translates to: + /// **'CURRENT TIME ZONE'** + String get currentTimeZone; + + /// No description provided for @commonTimeZones. + /// + /// In en, this message translates to: + /// **'COMMON TIME ZONES'** + String get commonTimeZones; + + /// No description provided for @dateFormat. + /// + /// In en, this message translates to: + /// **'DATE FORMAT'** + String get dateFormat; + + /// No description provided for @timeFormat. + /// + /// In en, this message translates to: + /// **'TIME FORMAT'** + String get timeFormat; + + /// No description provided for @hour12. + /// + /// In en, this message translates to: + /// **'12 Hour'** + String get hour12; + + /// No description provided for @hour24. + /// + /// In en, this message translates to: + /// **'24 Hour'** + String get hour24; + + /// No description provided for @firstDayOfWeek. + /// + /// In en, this message translates to: + /// **'FIRST DAY OF WEEK'** + String get firstDayOfWeek; + + /// No description provided for @storedInUtcNote. + /// + /// In en, this message translates to: + /// **'Stored in UTC - converted to your time zone on display'** + String get storedInUtcNote; + + /// No description provided for @savePreferences. + /// + /// In en, this message translates to: + /// **'Save Preferences'** + String get savePreferences; + + /// No description provided for @preferencesSaved. + /// + /// In en, this message translates to: + /// **'Preferences saved'** + String get preferencesSaved; } class _AppLocalizationsDelegate diff --git a/base_project/lib/l10n/app_localizations_en.dart b/base_project/lib/l10n/app_localizations_en.dart index 00abbe2..fae0c09 100644 --- a/base_project/lib/l10n/app_localizations_en.dart +++ b/base_project/lib/l10n/app_localizations_en.dart @@ -523,4 +523,44 @@ class AppLocalizationsEn extends AppLocalizations { @override String get successTitle => 'Success'; + + @override + String get timeZoneSettings => 'Time Zone Settings'; + + @override + String get timeZoneSubtitle => 'Set your local time, date & week preferences'; + + @override + String get livePreview => 'LIVE PREVIEW'; + + @override + String get currentTimeZone => 'CURRENT TIME ZONE'; + + @override + String get commonTimeZones => 'COMMON TIME ZONES'; + + @override + String get dateFormat => 'DATE FORMAT'; + + @override + String get timeFormat => 'TIME FORMAT'; + + @override + String get hour12 => '12 Hour'; + + @override + String get hour24 => '24 Hour'; + + @override + String get firstDayOfWeek => 'FIRST DAY OF WEEK'; + + @override + String get storedInUtcNote => + 'Stored in UTC - converted to your time zone on display'; + + @override + String get savePreferences => 'Save Preferences'; + + @override + String get preferencesSaved => 'Preferences saved'; } diff --git a/base_project/lib/l10n/app_localizations_hi.dart b/base_project/lib/l10n/app_localizations_hi.dart index 1252870..2626c6a 100644 --- a/base_project/lib/l10n/app_localizations_hi.dart +++ b/base_project/lib/l10n/app_localizations_hi.dart @@ -523,4 +523,45 @@ class AppLocalizationsHi extends AppLocalizations { @override String get successTitle => 'सफलता'; + + @override + String get timeZoneSettings => 'समय क्षेत्र सेटिंग'; + + @override + String get timeZoneSubtitle => + 'अपना स्थानीय समय, तिथि और सप्ताह वरीयताएँ सेट करें'; + + @override + String get livePreview => 'लाइव पूर्वावलोकन'; + + @override + String get currentTimeZone => 'वर्तमान समय क्षेत्र'; + + @override + String get commonTimeZones => 'सामान्य समय क्षेत्र'; + + @override + String get dateFormat => 'तिथि प्रारूप'; + + @override + String get timeFormat => 'समय प्रारूप'; + + @override + String get hour12 => '12 घंटे'; + + @override + String get hour24 => '24 घंटे'; + + @override + String get firstDayOfWeek => 'सप्ताह का पहला दिन'; + + @override + String get storedInUtcNote => + 'UTC में संग्रहीत - प्रदर्शन पर आपके समय क्षेत्र में बदला गया'; + + @override + String get savePreferences => 'वरीयताएँ सहेजें'; + + @override + String get preferencesSaved => 'वरीयताएँ सहेजी गईं'; } diff --git a/base_project/lib/main.dart b/base_project/lib/main.dart index c8815b2..d2e682c 100644 --- a/base_project/lib/main.dart +++ b/base_project/lib/main.dart @@ -9,7 +9,9 @@ import 'package:provider/provider.dart'; import 'core/providers/dynamic_theme_provider.dart'; import 'core/providers/locale_provider.dart'; import 'core/providers/theme_provider.dart'; +import 'core/providers/timezone_provider.dart'; import 'core/theme/app_theme.dart'; +import 'package:timezone/data/latest_all.dart' as tzdata; import 'l10n/app_localizations.dart'; import 'routes/app_routes.dart'; import 'utils/managers/user_manager.dart'; @@ -19,6 +21,10 @@ void main() async { await UserManager().initialize(); final localeProvider = LocaleProvider(); await localeProvider.initialize(); + // Time-zone display prefs (local-only v1): IANA DB + persisted selection. + tzdata.initializeTimeZones(); + final timezoneProvider = TimezoneProvider(); + await timezoneProvider.initialize(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); runApp(MultiProvider( @@ -29,6 +35,7 @@ void main() async { ChangeNotifierProvider(create: (context) => ThemeProvider()), ChangeNotifierProvider(create: (context) => DynamicThemeProvider()), ChangeNotifierProvider.value(value: localeProvider), + ChangeNotifierProvider.value(value: timezoneProvider), ], child: const MyApp(), )); diff --git a/base_project/lib/shared/widgets/timezone_settings_card.dart b/base_project/lib/shared/widgets/timezone_settings_card.dart new file mode 100644 index 0000000..afd6e8d --- /dev/null +++ b/base_project/lib/shared/widgets/timezone_settings_card.dart @@ -0,0 +1,383 @@ +import 'dart:async'; + +import 'package:base_project/core/constants/ui_constants.dart'; +import 'package:base_project/core/providers/dynamic_theme_provider.dart'; +import 'package:base_project/core/providers/locale_provider.dart'; +import 'package:base_project/core/providers/timezone_provider.dart'; +import 'package:base_project/l10n/app_localizations.dart'; +import 'package:base_project/resources/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +/// Time Zone Settings card, themed for base_project. +/// - Live ticking preview in the SELECTED zone (see time here / there). +/// - Current zone dropdown + common-zone quick chips. +/// - Date format / 12-24h / week-start prefs, persisted locally. +/// - Theming: surfaces + text from Theme colorScheme (dark-mode safe), +/// header/preview gradients from DynamicThemeProvider (logo-derived), +/// radii/spacing from UIConstants, inputs/buttons/chips from AppTheme. +/// - Multilingual: every label via AppLocalizations (EN/HI). +class TimezoneSettingsCard extends StatefulWidget { + const TimezoneSettingsCard({super.key}); + + @override + State createState() => _TimezoneSettingsCardState(); +} + +class _TimezoneSettingsCardState extends State { + Timer? _ticker; + + @override + void initState() { + super.initState(); + _ticker = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted) setState(() {}); + }); + } + + @override + void dispose() { + _ticker?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + final localeCode = context.watch().locale.languageCode; + // Listen to prefs so preview + chips update instantly on selection. + final tzPrefs = context.watch(); + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final gradients = + context.watch().getGradientColors(); + final gradStart = + gradients.isNotEmpty ? gradients.first : AppColors.primary; + final gradEnd = + gradients.length > 1 ? gradients.last : AppColors.primary2; + + final now = tzPrefs.nowInZone(); + final timePattern = tzPrefs.is24h ? 'HH:mm:ss' : 'hh:mm:ss a'; + final timeStr = DateFormat(timePattern, localeCode).format(now); + final dateLine = + DateFormat('EEEE, MMMM d, yyyy', localeCode).format(now); + final offset = tzPrefs.offsetLabel(now); + final city = TimezoneProvider.cityOf(tzPrefs.zoneId); + final onGradient = Colors.white; + + return Card( + margin: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header + Container( + padding: const EdgeInsets.fromLTRB(UIConstants.spacing16, + UIConstants.spacing14, UIConstants.spacing16, UIConstants.spacing14), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [gradStart, gradEnd], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(UIConstants.spacing8), + decoration: BoxDecoration( + color: onGradient.withValues(alpha: 0.2), + borderRadius: + BorderRadius.circular(UIConstants.radius12), + ), + child: Icon(Icons.public, + color: onGradient, size: UIConstants.iconSizeMedium), + ), + const SizedBox(width: UIConstants.spacing12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(strings.timeZoneSettings, + style: textTheme.titleMedium?.copyWith( + color: onGradient, + fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text(strings.timeZoneSubtitle, + style: textTheme.bodySmall + ?.copyWith(color: onGradient.withValues(alpha: 0.85))), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing12, + vertical: UIConstants.spacing4), + decoration: BoxDecoration( + color: onGradient.withValues(alpha: 0.2), + borderRadius: + BorderRadius.circular(UIConstants.radiusFull), + ), + child: Text('UTC', + style: TextStyle( + color: onGradient, + fontSize: 11, + fontWeight: FontWeight.w600)), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(UIConstants.spacing16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Live preview + Container( + padding: const EdgeInsets.all(UIConstants.spacing16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + gradStart.withValues(alpha: 0.9), + gradEnd.withValues(alpha: 0.9) + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + borderRadius: + BorderRadius.circular(UIConstants.radius16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('◷ ${strings.livePreview}', + style: TextStyle( + color: onGradient.withValues(alpha: 0.8), + fontSize: 11, + letterSpacing: 1.2)), + const SizedBox(height: UIConstants.spacing8), + Text(timeStr, + style: TextStyle( + color: onGradient, + fontSize: 38, + fontWeight: FontWeight.w800, + letterSpacing: 1)), + const SizedBox(height: 2), + Text(dateLine, + style: TextStyle( + color: onGradient.withValues(alpha: 0.9), + fontSize: 13)), + const SizedBox(height: UIConstants.spacing12), + Wrap( + spacing: UIConstants.spacing8, + runSpacing: UIConstants.spacing8, + children: [ + _miniChip('◉ $city', onGradient), + _miniChip(offset, onGradient), + _miniChip(tzPrefs.datePattern, onGradient), + _miniChip( + tzPrefs.is24h + ? strings.hour24 + : strings.hour12, + onGradient), + ], + ), + ], + ), + ), + const SizedBox(height: UIConstants.spacing16), + _sectionLabel(strings.currentTimeZone, colorScheme), + const SizedBox(height: UIConstants.spacing8), + DropdownButtonFormField( + key: ValueKey('zone_${tzPrefs.zoneId}'), + initialValue: tzPrefs.zoneId, + items: TimezoneProvider.commonZoneIds + .map((id) => DropdownMenuItem( + value: id, + child: Text(tzPrefs.displayLabel(id), + style: textTheme.bodyMedium))) + .toList(), + onChanged: (v) { + if (v != null) tzPrefs.setZone(v); + }, + ), + const SizedBox(height: UIConstants.spacing16), + _sectionLabel(strings.commonTimeZones, colorScheme), + const SizedBox(height: UIConstants.spacing8), + Wrap( + spacing: UIConstants.spacing8, + runSpacing: UIConstants.spacing8, + children: TimezoneProvider.commonZoneIds.map((id) { + return ChoiceChip( + label: Text(TimezoneProvider.cityOf(id)), + selected: id == tzPrefs.zoneId, + onSelected: (_) => tzPrefs.setZone(id), + ); + }).toList(), + ), + const SizedBox(height: UIConstants.spacing16), + LayoutBuilder(builder: (context, constraints) { + final narrow = constraints.maxWidth < 520; + final fields = [ + _labelled(strings.dateFormat, + DropdownButtonFormField( + key: ValueKey('pat_${tzPrefs.datePattern}'), + initialValue: tzPrefs.datePattern, + items: TimezoneProvider.datePatterns + .map((p) => DropdownMenuItem( + value: p, + child: Text(p, + style: textTheme.bodyMedium))) + .toList(), + onChanged: (v) { + if (v != null) tzPrefs.setDatePattern(v); + }, + )), + _labelled( + strings.timeFormat, + SegmentedButton( + segments: [ + ButtonSegment( + value: false, + label: Text(strings.hour12)), + ButtonSegment( + value: true, + label: Text(strings.hour24)), + ], + selected: {tzPrefs.is24h}, + onSelectionChanged: (s) => + tzPrefs.setIs24h(s.first), + )), + _labelled(strings.firstDayOfWeek, + DropdownButtonFormField( + key: ValueKey('ws_${tzPrefs.weekStart}'), + initialValue: tzPrefs.weekStart, + items: TimezoneProvider.weekDays + .map((d) => DropdownMenuItem( + value: d, + child: Text( + _localizedWeekday(d, localeCode), + style: textTheme.bodyMedium))) + .toList(), + onChanged: (v) { + if (v != null) tzPrefs.setWeekStart(v); + }, + )), + ]; + if (narrow) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + fields[0], + const SizedBox(height: UIConstants.spacing12), + fields[1], + const SizedBox(height: UIConstants.spacing12), + fields[2], + ], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: fields[0]), + const SizedBox(width: UIConstants.spacing8), + Expanded(child: fields[1]), + const SizedBox(width: UIConstants.spacing8), + Expanded(child: fields[2]), + ], + ); + }), + const SizedBox(height: UIConstants.spacing16), + Row( + children: [ + Icon(Icons.info_outline, + size: UIConstants.iconSizeSmall, + color: colorScheme.onSurfaceVariant), + const SizedBox(width: UIConstants.spacing8), + Expanded( + child: Text(strings.storedInUtcNote, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant)), + ), + ], + ), + const SizedBox(height: UIConstants.spacing12), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: () async { + await tzPrefs.save(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(strings.preferencesSaved)), + ); + } + }, + icon: const Icon(Icons.save_outlined, size: 16), + label: Text(strings.savePreferences), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _sectionLabel(String label, ColorScheme colorScheme) { + return Text('◉ $label', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: colorScheme.onSurfaceVariant)); + } + + Widget _miniChip(String text, Color onGradient) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing12, + vertical: UIConstants.spacing4), + decoration: BoxDecoration( + color: onGradient.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(UIConstants.radiusFull), + ), + child: Text(text, style: TextStyle(color: onGradient, fontSize: 11)), + ); + } + + Widget _labelled(String label, Widget child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Builder(builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Text(label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: colorScheme.onSurfaceVariant)); + }), + const SizedBox(height: UIConstants.spacing8), + child, + ], + ); + } + + /// Weekday names localized via intl for the current locale; values + /// stay canonical English for storage. + String _localizedWeekday(String canonical, String localeCode) { + // 2026-10-05 is a Monday; add index to get each weekday name. + final base = DateTime(2026, 10, 5); + final idx = TimezoneProvider.weekDays.indexOf(canonical); + if (idx < 0) return canonical; + return DateFormat('EEEE', localeCode) + .format(base.add(Duration(days: idx))); + } +} diff --git a/base_project/lib/view/dashboard/profile/profile.dart b/base_project/lib/view/dashboard/profile/profile.dart index 13854e3..7b2dd53 100644 --- a/base_project/lib/view/dashboard/profile/profile.dart +++ b/base_project/lib/view/dashboard/profile/profile.dart @@ -12,6 +12,7 @@ import 'package:provider/provider.dart'; import 'package:base_project/core/providers/dynamic_theme_provider.dart'; import 'package:base_project/core/constants/ui_constants.dart'; import 'package:base_project/shared/widgets/app_bar/modern_app_bar.dart'; +import 'package:base_project/shared/widgets/timezone_settings_card.dart'; import 'account_update.dart'; @@ -131,6 +132,17 @@ class _ProfileViewState extends State // Profile Actions Section _buildProfileActions(context, colorScheme), + + SizedBox( + height: UIConstants.getResponsiveSpacing( + context, + mobile: UIConstants.spacing32, + tablet: UIConstants.spacing40, + desktop: UIConstants.spacing48, + )), + + // Time Zone Settings Section (local display prefs) + const TimezoneSettingsCard(), ], ), ), diff --git a/base_project/pubspec.lock b/base_project/pubspec.lock index 55bcbb2..5110997 100644 --- a/base_project/pubspec.lock +++ b/base_project/pubspec.lock @@ -601,10 +601,10 @@ packages: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.6.0" http_parser: dependency: "direct main" description: @@ -1362,6 +1362,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.12" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb" + url: "https://pub.dev" + source: hosted + version: "0.11.1" typed_data: dependency: transitive description: diff --git a/base_project/pubspec.yaml b/base_project/pubspec.yaml index b1ffd26..3776eb6 100644 --- a/base_project/pubspec.yaml +++ b/base_project/pubspec.yaml @@ -49,7 +49,10 @@ dependencies: path: ^1.9.0 flutter_svg_provider: ^1.0.7 pinput: ^5.0.1 - intl: ^0.20.2 + # Wide range so both SDK pins resolve: Docker Flutter 3.41.1 pins intl + # 0.19.0 via flutter_localizations, local Flutter 3.47.x resolves 0.20.x. + intl: '>=0.19.0 <0.21.0' + timezone: ^0.11.1 http_parser: ^4.1.2 dotted_border: ^2.1.0 redis: ^4.0.0