From 881a665f14839b3577592831a14cb12f3c0bfd4a Mon Sep 17 00:00:00 2001 From: Azmat-7860 Date: Sat, 3 Oct 2026 15:00:55 +0530 Subject: [PATCH] adding multitimezone --- lib/l10n/app_en.arb | 28 +- lib/l10n/app_hi.arb | 15 +- lib/l10n/app_localizations.dart | 76 +++- lib/l10n/app_localizations_en.dart | 39 +- lib/l10n/app_localizations_hi.dart | 38 ++ lib/main.dart | 14 +- lib/providers/timezone_provider.dart | 179 ++++++++ .../profileManagement/Profilesettings.dart | 4 + lib/widgets/timezone_settings_card.dart | 417 ++++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 1 + 11 files changed, 805 insertions(+), 8 deletions(-) create mode 100644 lib/providers/timezone_provider.dart create mode 100644 lib/widgets/timezone_settings_card.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7127c90..24746cb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -646,5 +646,31 @@ "@pdfSaved": { "description": "PDF saved message", "placeholders": { "path": { "type": "String" } } - } + }, + "timeZoneSettings": "Time Zone Settings", + "@timeZoneSettings": { "description": "Timezone settings title" }, + "timeZoneSubtitle": "Set your local time, date & week preferences", + "@timeZoneSubtitle": { "description": "Timezone settings subtitle" }, + "livePreview": "LIVE PREVIEW", + "@livePreview": { "description": "Live preview label" }, + "currentTimeZone": "CURRENT TIME ZONE", + "@currentTimeZone": { "description": "Current timezone label" }, + "commonTimeZones": "COMMON TIME ZONES", + "@commonTimeZones": { "description": "Common zones label" }, + "dateFormat": "DATE FORMAT", + "@dateFormat": { "description": "Date format label" }, + "timeFormat": "TIME FORMAT", + "@timeFormat": { "description": "Time format label" }, + "hour12": "12 Hour", + "@hour12": { "description": "12 hour option" }, + "hour24": "24 Hour", + "@hour24": { "description": "24 hour option" }, + "firstDayOfWeek": "FIRST DAY OF WEEK", + "@firstDayOfWeek": { "description": "Week start label" }, + "storedInUtcNote": "Stored in UTC - converted to your time zone on display", + "@storedInUtcNote": { "description": "UTC storage note" }, + "savePreferences": "Save Preferences", + "@savePreferences": { "description": "Save preferences button" }, + "preferencesSaved": "Preferences saved", + "@preferencesSaved": { "description": "Preferences saved message" } } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 5443946..aa0cecc 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -273,5 +273,18 @@ "keyLabel": "कुंजी", "valueLabel": "मान", "dropHere": "यहां छोड़ें", - "pdfSaved": "PDF यहां सहेजा गया {path}" + "pdfSaved": "PDF यहां सहेजा गया {path}", + "timeZoneSettings": "समय क्षेत्र सेटिंग", + "timeZoneSubtitle": "अपना स्थानीय समय, तिथि और सप्ताह वरीयताएँ सेट करें", + "livePreview": "लाइव पूर्वावलोकन", + "currentTimeZone": "वर्तमान समय क्षेत्र", + "commonTimeZones": "सामान्य समय क्षेत्र", + "dateFormat": "तिथि प्रारूप", + "timeFormat": "समय प्रारूप", + "hour12": "12 घंटे", + "hour24": "24 घंटे", + "firstDayOfWeek": "सप्ताह का पहला दिन", + "storedInUtcNote": "UTC में संग्रहीत - प्रदर्शन पर आपके समय क्षेत्र में बदला गया", + "savePreferences": "वरीयताएँ सहेजें", + "preferencesSaved": "वरीयताएँ सहेजी गईं" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 12b1ba6..9d612ab 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1220,10 +1220,10 @@ abstract class AppLocalizations { /// **'Single Charge'** String get singleCharge; - /// First day field + /// Week start label /// /// In en, this message translates to: - /// **'First Day of The Week'** + /// **'FIRST DAY OF WEEK'** String get firstDayOfWeek; /// Hour per shift field @@ -1735,6 +1735,78 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'PDF saved to {path}'** String pdfSaved(String path); + + /// Timezone settings title + /// + /// In en, this message translates to: + /// **'Time Zone Settings'** + String get timeZoneSettings; + + /// Timezone settings subtitle + /// + /// In en, this message translates to: + /// **'Set your local time, date & week preferences'** + String get timeZoneSubtitle; + + /// Live preview label + /// + /// In en, this message translates to: + /// **'LIVE PREVIEW'** + String get livePreview; + + /// Current timezone label + /// + /// In en, this message translates to: + /// **'CURRENT TIME ZONE'** + String get currentTimeZone; + + /// Common zones label + /// + /// In en, this message translates to: + /// **'COMMON TIME ZONES'** + String get commonTimeZones; + + /// Date format label + /// + /// In en, this message translates to: + /// **'DATE FORMAT'** + String get dateFormat; + + /// Time format label + /// + /// In en, this message translates to: + /// **'TIME FORMAT'** + String get timeFormat; + + /// 12 hour option + /// + /// In en, this message translates to: + /// **'12 Hour'** + String get hour12; + + /// 24 hour option + /// + /// In en, this message translates to: + /// **'24 Hour'** + String get hour24; + + /// UTC storage note + /// + /// In en, this message translates to: + /// **'Stored in UTC - converted to your time zone on display'** + String get storedInUtcNote; + + /// Save preferences button + /// + /// In en, this message translates to: + /// **'Save Preferences'** + String get savePreferences; + + /// Preferences saved message + /// + /// In en, this message translates to: + /// **'Preferences saved'** + String get preferencesSaved; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5906f5b..e3062b3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -623,7 +623,7 @@ class AppLocalizationsEn extends AppLocalizations { String get singleCharge => 'Single Charge'; @override - String get firstDayOfWeek => 'First Day of The Week'; + String get firstDayOfWeek => 'FIRST DAY OF WEEK'; @override String get hourPerShift => 'Hour per Shift'; @@ -901,4 +901,41 @@ class AppLocalizationsEn extends AppLocalizations { String pdfSaved(String path) { return 'PDF saved to $path'; } + + @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 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/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 7cf5bd6..bfe1a91 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -901,4 +901,42 @@ class AppLocalizationsHi extends AppLocalizations { String pdfSaved(String path) { return 'PDF यहां सहेजा गया $path'; } + + @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 storedInUtcNote => + 'UTC में संग्रहीत - प्रदर्शन पर आपके समय क्षेत्र में बदला गया'; + + @override + String get savePreferences => 'वरीयताएँ सहेजें'; + + @override + String get preferencesSaved => 'वरीयताएँ सहेजी गईं'; } diff --git a/lib/main.dart b/lib/main.dart index 334da33..4be8a03 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,8 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqlite3/sqlite3.dart'; import 'providers/locale_provider.dart'; +import 'providers/timezone_provider.dart'; +import 'package:timezone/data/latest_all.dart' as tzdata; import 'l10n/app_localizations.dart'; import 'Utils/dynamic_strings.dart'; import 'screens/Attendance/attendance_service.dart'; @@ -49,6 +51,11 @@ void main() async { await localeProvider.loadServerCache(); await localeProvider.init(); + // Time-zone display prefs (local-only v1): IANA DB + persisted selection. + tzdata.initializeTimeZones(); + final timezoneProvider = TimezoneProvider(); + await timezoneProvider.init(); + //Request notification permissions // await _requestNotificationPermissions(); @@ -62,8 +69,11 @@ void main() async { // constraints: Constraints(networkType: NetworkType.connected), // ); runApp( - ChangeNotifierProvider.value( - value: localeProvider, + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: localeProvider), + ChangeNotifierProvider.value(value: timezoneProvider), + ], child: const MyApp(), ), ); diff --git a/lib/providers/timezone_provider.dart b/lib/providers/timezone_provider.dart new file mode 100644 index 0000000..9adcc8f --- /dev/null +++ b/lib/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 (v1: local-only). +/// Mirrors LocaleProvider: ChangeNotifier + SharedPreferences + notifyListeners. +/// Backend sync (AppUser.multitime) is a later phase; for now prefs stay on-device +/// so the settings card in the screenshot works standalone + multilingual. +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 matching the settings mock. + 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 init() 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 (button in the mock). Values are already persisted on + /// change; 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/lib/screens/profileManagement/Profilesettings.dart b/lib/screens/profileManagement/Profilesettings.dart index 4ca04b2..ede556d 100644 --- a/lib/screens/profileManagement/Profilesettings.dart +++ b/lib/screens/profileManagement/Profilesettings.dart @@ -16,6 +16,7 @@ import '../../widgets/app_bar/custom_app_bar.dart'; import '../../widgets/custom_button.dart'; import '../../widgets/custom_text_form_field.dart'; import '../../widgets/language_switcher.dart'; +import '../../widgets/timezone_settings_card.dart'; import '../Login Screen/login_screen.dart'; import '../LogoutService/Logoutservice.dart'; import 'apiserviceprofilemanagement.dart'; @@ -357,6 +358,9 @@ class _ProfileSettingsScreenState extends State { SizedBox(height: 10), const LanguageSwitcher(), SizedBox(height: 20), + // Section: Time zone display prefs (local-only v1, multilingual via ARB) + const TimezoneSettingsCard(), + SizedBox(height: 20), // Section: Profile Form Text( AppLocalizations.of(context)!.yourProfileInfo, diff --git a/lib/widgets/timezone_settings_card.dart b/lib/widgets/timezone_settings_card.dart new file mode 100644 index 0000000..88a68a8 --- /dev/null +++ b/lib/widgets/timezone_settings_card.dart @@ -0,0 +1,417 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +import '../l10n/app_localizations.dart'; +import '../providers/locale_provider.dart'; +import '../providers/timezone_provider.dart'; + +/// Standalone Time Zone Settings card (matches mock). +/// - 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, all persisted locally. +/// - Fully multilingual: every label via AppLocalizations, so the EN/HI +/// LanguageSwitcher rebuilds this card with no extra work. +class TimezoneSettingsCard extends StatefulWidget { + const TimezoneSettingsCard({super.key}); + + @override + State createState() => _TimezoneSettingsCardState(); +} + +class _TimezoneSettingsCardState extends State { + Timer? _ticker; + DateTime _tick = DateTime.now(); + + @override + void initState() { + super.initState(); + _ticker = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted) setState(() => _tick = DateTime.now()); + }); + } + + @override + void dispose() { + _ticker?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final localeCode = Provider.of(context).code; + // Listen to prefs so preview + chips update instantly on selection. + final tzPrefs = Provider.of(context); + // Keep linter happy about the 1s tick even though zone time derives + // from provider clock below. + _tick; + + 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); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header + Container( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF3B3BD6), Color(0xFF7B2FF7)], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.public, + color: Colors.white, size: 22), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.timeZoneSettings, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text(l10n.timeZoneSubtitle, + style: TextStyle( + color: + Colors.white.withValues(alpha: 0.85), + fontSize: 11)), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Text('UTC', + style: TextStyle(color: Colors.white, fontSize: 11)), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Live preview + Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1E2A78), Color(0xFF7B2FF7)], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('◷ ${l10n.livePreview}', + style: TextStyle( + color: + Colors.white.withValues(alpha: 0.8), + fontSize: 11, + letterSpacing: 1.2)), + const SizedBox(height: 6), + Text(timeStr, + style: const TextStyle( + color: Colors.white, + fontSize: 38, + fontWeight: FontWeight.w800, + letterSpacing: 1)), + const SizedBox(height: 2), + Text(dateLine, + style: TextStyle( + color: + Colors.white.withValues(alpha: 0.9), + fontSize: 13)), + const SizedBox(height: 10), + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + _miniChip('◉ $city'), + _miniChip(offset), + _miniChip(tzPrefs.datePattern), + _miniChip(tzPrefs.is24h + ? l10n.hour24 + : l10n.hour12), + ], + ), + ], + ), + ), + const SizedBox(height: 14), + Text('◉ ${l10n.currentTimeZone}', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: Colors.black54)), + const SizedBox(height: 6), + DropdownButtonFormField( + // ignore: deprecated_member_use + value: tzPrefs.zoneId, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + ), + items: TimezoneProvider.commonZoneIds + .map((id) => DropdownMenuItem( + value: id, + child: Text(tzPrefs.displayLabel(id), + style: const TextStyle(fontSize: 13)))) + .toList(), + onChanged: (v) { + if (v != null) tzPrefs.setZone(v); + }, + ), + const SizedBox(height: 12), + Text('★ ${l10n.commonTimeZones}', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: Colors.black54)), + const SizedBox(height: 6), + Wrap( + spacing: 6, + runSpacing: 6, + children: TimezoneProvider.commonZoneIds.map((id) { + final selected = id == tzPrefs.zoneId; + return ChoiceChip( + label: Text( + TimezoneProvider.cityOf(id), + style: TextStyle( + fontSize: 11, + color: selected + ? Colors.white + : Colors.black87)), + selected: selected, + selectedColor: const Color(0xFF7B2FF7), + backgroundColor: Colors.grey.shade100, + onSelected: (_) => tzPrefs.setZone(id), + ); + }).toList(), + ), + const SizedBox(height: 12), + LayoutBuilder(builder: (context, constraints) { + final narrow = constraints.maxWidth < 520; + Widget row(Widget a, Widget b, Widget c) { + if (narrow) { + return Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [a, const SizedBox(height: 8), b, const SizedBox(height: 8), c], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: a), + const SizedBox(width: 8), + Expanded(child: b), + const SizedBox(width: 8), + Expanded(child: c), + ], + ); + } + return row( + _labelled(l10n.dateFormat, + DropdownButtonFormField( + // ignore: deprecated_member_use + value: tzPrefs.datePattern, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 10), + ), + items: TimezoneProvider.datePatterns + .map((p) => DropdownMenuItem( + value: p, + child: Text(p, + style: const TextStyle( + fontSize: 12)))) + .toList(), + onChanged: (v) { + if (v != null) tzPrefs.setDatePattern(v); + }, + )), + _labelled( + l10n.timeFormat, + SegmentedButton( + style: const ButtonStyle( + visualDensity: VisualDensity.compact), + segments: [ + ButtonSegment( + value: false, label: Text(l10n.hour12)), + ButtonSegment( + value: true, label: Text(l10n.hour24)), + ], + selected: {tzPrefs.is24h}, + onSelectionChanged: (s) => + tzPrefs.setIs24h(s.first), + )), + _labelled(l10n.firstDayOfWeek, + DropdownButtonFormField( + // ignore: deprecated_member_use + value: tzPrefs.weekStart, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 10), + ), + items: TimezoneProvider.weekDays + .map((d) => DropdownMenuItem( + value: d, + child: Text( + _localizedWeekday( + d, localeCode), + style: const TextStyle( + fontSize: 12)))) + .toList(), + onChanged: (v) { + if (v != null) tzPrefs.setWeekStart(v); + }, + )), + ); + }), + const SizedBox(height: 12), + Row( + children: [ + const Icon(Icons.info_outline, + size: 14, color: Colors.black45), + const SizedBox(width: 6), + Expanded( + child: Text(l10n.storedInUtcNote, + style: const TextStyle( + fontSize: 11, color: Colors.black54)), + ), + ], + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient(colors: [ + Color(0xFF3B3BD6), + Color(0xFFE040FB) + ]), + borderRadius: + BorderRadius.all(Radius.circular(12)), + ), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + onPressed: () async { + await tzPrefs.save(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(l10n.preferencesSaved)), + ); + } + }, + icon: const Icon(Icons.save_outlined, + size: 16, color: Colors.white), + label: Text(l10n.savePreferences, + style: const TextStyle(color: Colors.white)), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _miniChip(String text) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(20), + ), + child: Text(text, + style: const TextStyle(color: Colors.white, fontSize: 11)), + ); + } + + Widget _labelled(String label, Widget child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: Colors.black54)), + const SizedBox(height: 6), + 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/pubspec.lock b/pubspec.lock index 322c98d..2767d3e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1683,7 +1683,7 @@ packages: source: hosted version: "0.7.12" timezone: - dependency: transitive + dependency: "direct main" description: name: timezone sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb" diff --git a/pubspec.yaml b/pubspec.yaml index c08e18a..f3b2188 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dependencies: flutter_svg_provider: ^1.0.7 pinput: ^6.0.2 intl: ^0.20.2 + timezone: ^0.11.1 sqlite3: ^3.5.2 connectivity_plus: ^7.3.1 flutter_foreground_task: ^9.1.0