feat(i18n): localize onboarding & reports; logout
- add en/ms keys for welcome, onboarding, capture/review, errors, and settings labels - replace hardcoded strings with I18n.t in WelcomeScreen, OnboardingFlow, CaptureScreen, ReviewScreen, Map severity legend, and Settings - update StartRouter: onboarded guests go to MainScreen; non-guest users see SignIn; skip marks user as onboarded guest - add logout action in Settings that clears onboarding/user mode prefs and returns to the welcome flow - refine settings diagnostics and app section; move labels to i18n - leave TODOs for app subtitle, processing status, and ReportCard menu labels for future i18n coverage
This commit is contained in:
56
lib/app.dart
56
lib/app.dart
@@ -281,10 +281,6 @@ class _StartRouterState extends State<StartRouter> {
|
||||
if (_loading) {
|
||||
screen = const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
screenKey = 'loading';
|
||||
} else if (_userMode == 'guest') {
|
||||
// If user is known to be in guest mode, take them to sign-in / sign-up first
|
||||
screen = const SignInScreen();
|
||||
screenKey = 'signin';
|
||||
} else if (!_onboarded) {
|
||||
screen = WelcomeScreen(
|
||||
onContinue: () async {
|
||||
@@ -305,14 +301,19 @@ class _StartRouterState extends State<StartRouter> {
|
||||
);
|
||||
},
|
||||
onSkip: () async {
|
||||
// Mark as onboarded and guest so next app start opens sign-up/sign-in
|
||||
// Mark as onboarded and guest so next app start goes to main app
|
||||
await _setOnboarded(asGuest: true);
|
||||
},
|
||||
);
|
||||
screenKey = 'welcome';
|
||||
} else {
|
||||
} else if (_userMode == 'guest') {
|
||||
// User is onboarded and in guest mode, take them to main app
|
||||
screen = const MainScreen();
|
||||
screenKey = 'main';
|
||||
} else {
|
||||
// User is onboarded but not in guest mode, show sign-in screen
|
||||
screen = const SignInScreen();
|
||||
screenKey = 'signin';
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
@@ -395,7 +396,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'FixMate',
|
||||
I18n.t('app.name'),
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -403,7 +404,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Civic Solutions',
|
||||
'Civic Solutions', // TODO: Add to i18n as app.subtitle
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: cs.onSurface.withOpacity(0.7)),
|
||||
),
|
||||
@@ -445,7 +446,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Spot it. Snap it. Fix it.',
|
||||
I18n.t('welcome.title'),
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -455,7 +456,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Report city issues in seconds with AI-powered detection. Help create safer, better communities together.',
|
||||
I18n.t('welcome.subtitle'),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: cs.onSurface.withOpacity(0.8),
|
||||
height: 1.5,
|
||||
@@ -473,7 +474,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
onPressed: onContinue,
|
||||
icon: const Icon(Icons.arrow_forward, size: 20),
|
||||
label: Text(
|
||||
'Continue as Guest',
|
||||
I18n.t('cta.continueGuest'),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -493,7 +494,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
onPressed: onSignIn,
|
||||
icon: const Icon(Icons.login, size: 20),
|
||||
label: Text(
|
||||
'Sign In',
|
||||
I18n.t('cta.signIn'),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
@@ -509,7 +510,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: onSkip,
|
||||
child: Text(
|
||||
'Skip for now',
|
||||
I18n.t('cta.skip'),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
@@ -646,7 +647,7 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Welcome to FixMate',
|
||||
I18n.t('onboarding.header'),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: cs.onSurface,
|
||||
@@ -662,28 +663,25 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
onPageChanged: (i) => setState(() => _index = i),
|
||||
children: [
|
||||
_buildPage(
|
||||
title: 'Fast Issue Reporting',
|
||||
subtitle: 'AI-Powered Detection',
|
||||
description:
|
||||
'Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.',
|
||||
title: I18n.t('onboarding.title1'),
|
||||
subtitle: I18n.t('onboarding.subtitle1'),
|
||||
description: I18n.t('onboarding.body1'),
|
||||
icon: Icons.camera_alt,
|
||||
gradientStart: const Color(0xFF22C55E),
|
||||
gradientEnd: const Color(0xFF4ADE80),
|
||||
),
|
||||
_buildPage(
|
||||
title: 'Smart City Mapping',
|
||||
subtitle: 'Real-Time Visualization',
|
||||
description:
|
||||
'View all reported issues on an interactive map with intelligent clustering and filtering options.',
|
||||
title: I18n.t('onboarding.title2'),
|
||||
subtitle: I18n.t('onboarding.subtitle2'),
|
||||
description: I18n.t('onboarding.body2'),
|
||||
icon: Icons.map,
|
||||
gradientStart: const Color(0xFF2563EB),
|
||||
gradientEnd: const Color(0xFF3B82F6),
|
||||
),
|
||||
_buildPage(
|
||||
title: 'Track Progress',
|
||||
subtitle: 'Stay Informed',
|
||||
description:
|
||||
'Follow the status of your reports from submission to resolution. Help make your community better.',
|
||||
title: I18n.t('onboarding.title3'),
|
||||
subtitle: I18n.t('onboarding.subtitle3'),
|
||||
description: I18n.t('onboarding.body3'),
|
||||
icon: Icons.check_circle,
|
||||
gradientStart: const Color(0xFFF97316),
|
||||
gradientEnd: const Color(0xFFFB923C),
|
||||
@@ -717,7 +715,7 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(
|
||||
'Skip',
|
||||
I18n.t('onboarding.skip'),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
@@ -739,7 +737,9 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_index < 2 ? 'Next' : 'Get Started',
|
||||
_index < 2
|
||||
? I18n.t('onboarding.next')
|
||||
: I18n.t('onboarding.getStarted'),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -679,7 +679,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Issue Severity',
|
||||
I18n.t('label.severity'),
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -691,11 +691,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_enhancedLegendItem(Severity.high, 'High Priority'),
|
||||
_enhancedLegendItem(
|
||||
Severity.high,
|
||||
'${I18n.t('severity.high')} Priority',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_enhancedLegendItem(Severity.medium, 'Medium Priority'),
|
||||
_enhancedLegendItem(
|
||||
Severity.medium,
|
||||
'${I18n.t('severity.medium')} Priority',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_enhancedLegendItem(Severity.low, 'Low Priority'),
|
||||
_enhancedLegendItem(
|
||||
Severity.low,
|
||||
'${I18n.t('severity.low')} Priority',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -37,9 +37,11 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error picking image: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(I18n.t('error.imagePick', {'0': e.toString()})),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -58,8 +60,10 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
if (position == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Unable to get location. Please try again.'),
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'Unable to get location. Please try again.',
|
||||
), // TODO: Move to i18n
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -110,9 +114,11 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error processing image: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(I18n.t('error.imageProcessing', {'0': e.toString()})),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +133,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Report Issue',
|
||||
I18n.t('capture.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -181,7 +187,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Report City Issues',
|
||||
I18n.t('capture.subtitle'),
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -191,7 +197,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Take a photo of any urban maintenance issue like potholes, broken streetlights, or damaged signage.',
|
||||
I18n.t('capture.description'),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
height: 1.5,
|
||||
@@ -221,7 +227,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Processing image...',
|
||||
'Processing image...', // TODO: Move to i18n
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
@@ -249,7 +255,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
onPressed: () => _pickImage(ImageSource.camera),
|
||||
icon: const Icon(Icons.camera_alt, size: 24),
|
||||
label: const Text(
|
||||
'Take Photo',
|
||||
'Take Photo', // TODO: Move to i18n
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -274,7 +280,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
|
||||
onPressed: () => _pickImage(ImageSource.gallery),
|
||||
icon: const Icon(Icons.photo_library, size: 24),
|
||||
label: const Text(
|
||||
'Choose from Gallery',
|
||||
'Choose from Gallery', // TODO: Move to i18n
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -66,9 +66,9 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error saving report: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(I18n.t('error.saving', {'0': e.toString()}))),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -86,7 +86,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Review & Submit',
|
||||
I18n.t('review.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -102,7 +102,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(
|
||||
'Submit',
|
||||
I18n.t('review.submit'),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -190,7 +190,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'AI Analysis Complete',
|
||||
I18n.t('review.aiAnalysis'),
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -198,7 +198,13 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Smart detection with ${(widget.report.aiSuggestion.confidence * 100).round()}% confidence',
|
||||
I18n.t('review.aiConfidence', {
|
||||
'0':
|
||||
(widget.report.aiSuggestion.confidence *
|
||||
100)
|
||||
.round()
|
||||
.toString(),
|
||||
}),
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
@@ -300,7 +306,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.check_circle, size: 18),
|
||||
label: const Text('Use AI Suggestion'),
|
||||
label: Text(I18n.t('review.useSuggestion')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0EA5E9),
|
||||
foregroundColor: Colors.white,
|
||||
@@ -316,7 +322,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.edit, size: 18),
|
||||
label: const Text('Edit Manually'),
|
||||
label: Text(I18n.t('review.editManually')),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(
|
||||
color: const Color(0xFF0EA5E9),
|
||||
@@ -359,7 +365,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
Icon(Icons.category, color: cs.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Issue Category',
|
||||
I18n.t('review.category'),
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -443,7 +449,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
Icon(Icons.warning, color: cs.secondary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Severity Level',
|
||||
I18n.t('review.severity'),
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -539,7 +545,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
Icon(Icons.note, color: cs.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Additional Notes',
|
||||
I18n.t('review.notes'),
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -552,7 +558,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
TextField(
|
||||
controller: _notesController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Add any additional details or context...',
|
||||
hintText: I18n.t('review.notesHint'),
|
||||
hintStyle: TextStyle(
|
||||
color: cs.onSurface.withOpacity(0.6),
|
||||
),
|
||||
@@ -602,7 +608,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
Icon(Icons.location_on, color: cs.secondary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Location Details',
|
||||
I18n.t('review.location'),
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -630,7 +636,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Coordinates',
|
||||
I18n.t('review.coordinates'),
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
@@ -659,7 +665,10 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Accuracy: ${widget.report.location.accuracy!.toStringAsFixed(1)}m',
|
||||
I18n.t('review.accuracy', {
|
||||
'0': widget.report.location.accuracy!
|
||||
.toStringAsFixed(1),
|
||||
}),
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../l10n/i18n.dart';
|
||||
import '../../l10n/locale_provider.dart';
|
||||
import '../../services/storage.dart';
|
||||
@@ -42,8 +43,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
title: Text(I18n.t('confirm.clearData.title')),
|
||||
content: Text(I18n.t('confirm.clearData.message')),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(I18n.t('btn.no'))),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: Text(I18n.t('btn.yes'))),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(I18n.t('btn.no')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(I18n.t('btn.yes')),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -59,25 +66,59 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (ok) {
|
||||
await _loadStats();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(I18n.t('toast.storageCleared'))));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(I18n.t('toast.storageCleared'))),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to clear data')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(I18n.t('error.clearData'))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(I18n.t('confirm.logout.title')),
|
||||
content: Text(I18n.t('confirm.logout.message')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(I18n.t('btn.no')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(I18n.t('btn.yes')),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
// Clear user preferences to reset to initial state
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('onboarded_v1');
|
||||
await prefs.remove('user_mode');
|
||||
|
||||
if (mounted) {
|
||||
// Navigate back to the start router (welcome screen)
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localeProvider = Provider.of<LocaleProvider>(context);
|
||||
final isEnglish = localeProvider.isEnglish;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(I18n.t('nav.settings')),
|
||||
),
|
||||
appBar: AppBar(title: Text(I18n.t('nav.settings'))),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
@@ -110,25 +151,51 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
ListTile(
|
||||
title: Text(I18n.t('settings.diagnostics')),
|
||||
subtitle: _loadingStats
|
||||
? const Text('Loading...')
|
||||
: Text('${I18n.t('toast.reportSaved')}: ${_stats?.reportCount ?? 0} • ${_stats?.formattedPhotoSize ?? "0 B"}'),
|
||||
? const Text('Loading...') // TODO: Move to i18n
|
||||
: Text(
|
||||
'${I18n.t('toast.reportSaved')}: ${_stats?.reportCount ?? 0} • ${_stats?.formattedPhotoSize ?? "0 B"}',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _clearing ? null : () => _confirmAndClearAll(context),
|
||||
icon: const Icon(Icons.delete_forever),
|
||||
label: _clearing ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : Text(I18n.t('btn.clearAll')),
|
||||
label: _clearing
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(I18n.t('btn.clearAll')),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('App', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(
|
||||
I18n.t('settings.app'),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
title: Text(I18n.t('app.name')),
|
||||
subtitle: Text('v1.0.0'),
|
||||
subtitle: Text(I18n.t('settings.version')),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(I18n.t('settings.account')),
|
||||
subtitle: Text(I18n.t('settings.account.guest')),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _logout(),
|
||||
icon: const Icon(Icons.logout),
|
||||
label: Text(I18n.t('btn.logout')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +380,9 @@ class ReportCard extends StatelessWidget {
|
||||
children: [
|
||||
Icon(Icons.visibility),
|
||||
SizedBox(width: 8),
|
||||
Text('View Details'),
|
||||
Text(
|
||||
'View Details',
|
||||
), // TODO: Move to i18n but need to handle dynamic text in popup menu
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -390,7 +392,9 @@ class ReportCard extends StatelessWidget {
|
||||
children: [
|
||||
Icon(Icons.update),
|
||||
SizedBox(width: 8),
|
||||
Text('Update Status'),
|
||||
Text(
|
||||
'Update Status',
|
||||
), // TODO: Move to i18n but need to handle dynamic text in popup menu
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -400,7 +404,10 @@ class ReportCard extends StatelessWidget {
|
||||
children: [
|
||||
Icon(Icons.delete, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Text('Delete', style: TextStyle(color: Colors.red)),
|
||||
Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
), // TODO: Move to i18n but need to handle dynamic text in popup menu
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user