adding multitimezone
This commit is contained in:
+27
-1
@@ -646,5 +646,31 @@
|
|||||||
"@pdfSaved": {
|
"@pdfSaved": {
|
||||||
"description": "PDF saved message",
|
"description": "PDF saved message",
|
||||||
"placeholders": { "path": { "type": "String" } }
|
"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" }
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -273,5 +273,18 @@
|
|||||||
"keyLabel": "कुंजी",
|
"keyLabel": "कुंजी",
|
||||||
"valueLabel": "मान",
|
"valueLabel": "मान",
|
||||||
"dropHere": "यहां छोड़ें",
|
"dropHere": "यहां छोड़ें",
|
||||||
"pdfSaved": "PDF यहां सहेजा गया {path}"
|
"pdfSaved": "PDF यहां सहेजा गया {path}",
|
||||||
|
"timeZoneSettings": "समय क्षेत्र सेटिंग",
|
||||||
|
"timeZoneSubtitle": "अपना स्थानीय समय, तिथि और सप्ताह वरीयताएँ सेट करें",
|
||||||
|
"livePreview": "लाइव पूर्वावलोकन",
|
||||||
|
"currentTimeZone": "वर्तमान समय क्षेत्र",
|
||||||
|
"commonTimeZones": "सामान्य समय क्षेत्र",
|
||||||
|
"dateFormat": "तिथि प्रारूप",
|
||||||
|
"timeFormat": "समय प्रारूप",
|
||||||
|
"hour12": "12 घंटे",
|
||||||
|
"hour24": "24 घंटे",
|
||||||
|
"firstDayOfWeek": "सप्ताह का पहला दिन",
|
||||||
|
"storedInUtcNote": "UTC में संग्रहीत - प्रदर्शन पर आपके समय क्षेत्र में बदला गया",
|
||||||
|
"savePreferences": "वरीयताएँ सहेजें",
|
||||||
|
"preferencesSaved": "वरीयताएँ सहेजी गईं"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1220,10 +1220,10 @@ abstract class AppLocalizations {
|
|||||||
/// **'Single Charge'**
|
/// **'Single Charge'**
|
||||||
String get singleCharge;
|
String get singleCharge;
|
||||||
|
|
||||||
/// First day field
|
/// Week start label
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'First Day of The Week'**
|
/// **'FIRST DAY OF WEEK'**
|
||||||
String get firstDayOfWeek;
|
String get firstDayOfWeek;
|
||||||
|
|
||||||
/// Hour per shift field
|
/// Hour per shift field
|
||||||
@@ -1735,6 +1735,78 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'PDF saved to {path}'**
|
/// **'PDF saved to {path}'**
|
||||||
String pdfSaved(String 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
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -623,7 +623,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get singleCharge => 'Single Charge';
|
String get singleCharge => 'Single Charge';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get firstDayOfWeek => 'First Day of The Week';
|
String get firstDayOfWeek => 'FIRST DAY OF WEEK';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hourPerShift => 'Hour per Shift';
|
String get hourPerShift => 'Hour per Shift';
|
||||||
@@ -901,4 +901,41 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String pdfSaved(String path) {
|
String pdfSaved(String path) {
|
||||||
return 'PDF saved to $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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -901,4 +901,42 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||||||
String pdfSaved(String path) {
|
String pdfSaved(String path) {
|
||||||
return 'PDF यहां सहेजा गया $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 => 'वरीयताएँ सहेजी गईं';
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -7,6 +7,8 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
|
|
||||||
import 'providers/locale_provider.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 'l10n/app_localizations.dart';
|
||||||
import 'Utils/dynamic_strings.dart';
|
import 'Utils/dynamic_strings.dart';
|
||||||
import 'screens/Attendance/attendance_service.dart';
|
import 'screens/Attendance/attendance_service.dart';
|
||||||
@@ -49,6 +51,11 @@ void main() async {
|
|||||||
await localeProvider.loadServerCache();
|
await localeProvider.loadServerCache();
|
||||||
await localeProvider.init();
|
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
|
//Request notification permissions
|
||||||
// await _requestNotificationPermissions();
|
// await _requestNotificationPermissions();
|
||||||
|
|
||||||
@@ -62,8 +69,11 @@ void main() async {
|
|||||||
// constraints: Constraints(networkType: NetworkType.connected),
|
// constraints: Constraints(networkType: NetworkType.connected),
|
||||||
// );
|
// );
|
||||||
runApp(
|
runApp(
|
||||||
ChangeNotifierProvider.value(
|
MultiProvider(
|
||||||
value: localeProvider,
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: localeProvider),
|
||||||
|
ChangeNotifierProvider.value(value: timezoneProvider),
|
||||||
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<void> 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<void> setZone(String id) async {
|
||||||
|
_zoneId = id;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_zoneKey, id);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDatePattern(String p) async {
|
||||||
|
if (!datePatterns.contains(p)) return;
|
||||||
|
_datePattern = p;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_patternKey, p);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setIs24h(bool v) async {
|
||||||
|
_is24h = v;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_is24hKey, v);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import '../../widgets/app_bar/custom_app_bar.dart';
|
|||||||
import '../../widgets/custom_button.dart';
|
import '../../widgets/custom_button.dart';
|
||||||
import '../../widgets/custom_text_form_field.dart';
|
import '../../widgets/custom_text_form_field.dart';
|
||||||
import '../../widgets/language_switcher.dart';
|
import '../../widgets/language_switcher.dart';
|
||||||
|
import '../../widgets/timezone_settings_card.dart';
|
||||||
import '../Login Screen/login_screen.dart';
|
import '../Login Screen/login_screen.dart';
|
||||||
import '../LogoutService/Logoutservice.dart';
|
import '../LogoutService/Logoutservice.dart';
|
||||||
import 'apiserviceprofilemanagement.dart';
|
import 'apiserviceprofilemanagement.dart';
|
||||||
@@ -357,6 +358,9 @@ class _ProfileSettingsScreenState extends State<ProfileSettingsScreen> {
|
|||||||
SizedBox(height: 10),
|
SizedBox(height: 10),
|
||||||
const LanguageSwitcher(),
|
const LanguageSwitcher(),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
|
// Section: Time zone display prefs (local-only v1, multilingual via ARB)
|
||||||
|
const TimezoneSettingsCard(),
|
||||||
|
SizedBox(height: 20),
|
||||||
// Section: Profile Form
|
// Section: Profile Form
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.yourProfileInfo,
|
AppLocalizations.of(context)!.yourProfileInfo,
|
||||||
|
|||||||
@@ -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<TimezoneSettingsCard> createState() => _TimezoneSettingsCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimezoneSettingsCardState extends State<TimezoneSettingsCard> {
|
||||||
|
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<LocaleProvider>(context).code;
|
||||||
|
// Listen to prefs so preview + chips update instantly on selection.
|
||||||
|
final tzPrefs = Provider.of<TimezoneProvider>(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<String>(
|
||||||
|
// 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<String>(
|
||||||
|
// 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<bool>(
|
||||||
|
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<String>(
|
||||||
|
// 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1683,7 +1683,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.12"
|
version: "0.7.12"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: timezone
|
name: timezone
|
||||||
sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb"
|
sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb"
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ dependencies:
|
|||||||
flutter_svg_provider: ^1.0.7
|
flutter_svg_provider: ^1.0.7
|
||||||
pinput: ^6.0.2
|
pinput: ^6.0.2
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
|
timezone: ^0.11.1
|
||||||
sqlite3: ^3.5.2
|
sqlite3: ^3.5.2
|
||||||
connectivity_plus: ^7.3.1
|
connectivity_plus: ^7.3.1
|
||||||
flutter_foreground_task: ^9.1.0
|
flutter_foreground_task: ^9.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user