Files
authsec_flutter/lib/widgets/timezone_settings_card.dart
T
2026-10-03 15:00:55 +05:30

418 lines
16 KiB
Dart

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)));
}
}