多语言支持
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
import '../services/storage_service.dart';
|
||||
@@ -21,6 +22,7 @@ class CompleteScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final storageService = StorageService();
|
||||
final todayTotal = storageService.getTodayTotalMinutes();
|
||||
final todayDistractions = storageService.getTodayDistractionCount();
|
||||
@@ -44,12 +46,12 @@ class CompleteScreen extends StatelessWidget {
|
||||
|
||||
// You focused for X minutes
|
||||
Text(
|
||||
'You focused for',
|
||||
l10n.youFocusedFor,
|
||||
style: AppTextStyles.headline,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'$focusedMinutes ${focusedMinutes == 1 ? 'minute' : 'minutes'}',
|
||||
l10n.minutesValue(focusedMinutes, l10n.minutes(focusedMinutes)),
|
||||
style: AppTextStyles.largeNumber,
|
||||
),
|
||||
|
||||
@@ -67,12 +69,12 @@ class CompleteScreen extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Total Today: $todayTotal mins',
|
||||
l10n.totalToday(todayTotal),
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Distractions: $todayDistractions ${todayDistractions == 1 ? 'time' : 'times'}',
|
||||
l10n.distractionsCount(todayDistractions, l10n.times(todayDistractions)),
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -101,7 +103,7 @@ class CompleteScreen extends StatelessWidget {
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
child: const Text('Start Another'),
|
||||
child: Text(l10n.startAnother),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -118,7 +120,7 @@ class CompleteScreen extends StatelessWidget {
|
||||
(route) => route.isFirst, // Keep only the home screen in stack
|
||||
);
|
||||
},
|
||||
child: const Text('View History'),
|
||||
child: Text(l10n.viewHistory),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
import '../models/distraction_type.dart';
|
||||
@@ -76,11 +77,15 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _showBackgroundNotification() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minutes = _remainingSeconds ~/ 60;
|
||||
final seconds = _remainingSeconds % 60;
|
||||
final timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
_notificationService.showOngoingFocusNotification(
|
||||
remainingMinutes: minutes,
|
||||
remainingSeconds: seconds,
|
||||
title: l10n.notificationFocusInProgress,
|
||||
timeRemainingText: l10n.notificationRemaining(timeStr),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,11 +99,15 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
// Update background notification every 30 seconds when in background
|
||||
if (_isInBackground && _remainingSeconds > 0) {
|
||||
if (_remainingSeconds % 30 == 0) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minutes = _remainingSeconds ~/ 60;
|
||||
final seconds = _remainingSeconds % 60;
|
||||
final timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
_notificationService.updateOngoingFocusNotification(
|
||||
remainingMinutes: minutes,
|
||||
remainingSeconds: seconds,
|
||||
title: l10n.notificationFocusInProgress,
|
||||
timeRemainingText: l10n.notificationRemaining(timeStr),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -118,10 +127,26 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
|
||||
_saveFocusSession(completed: true);
|
||||
|
||||
// Send completion notification
|
||||
if (!mounted) return;
|
||||
|
||||
// Send completion notification with localized text
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minuteText = l10n.minutes(widget.durationMinutes);
|
||||
final notificationBody = _distractions.isEmpty
|
||||
? l10n.notificationFocusCompleteBodyNoDistractions(
|
||||
widget.durationMinutes,
|
||||
minuteText,
|
||||
)
|
||||
: l10n.notificationFocusCompleteBody(
|
||||
widget.durationMinutes,
|
||||
minuteText,
|
||||
);
|
||||
|
||||
await _notificationService.showFocusCompletedNotification(
|
||||
minutes: widget.durationMinutes,
|
||||
distractionCount: _distractions.length,
|
||||
title: l10n.notificationFocusCompleteTitle,
|
||||
body: notificationBody,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
@@ -152,20 +177,22 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _stopEarly() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final actualMinutes = ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
|
||||
final minuteText = actualMinutes == 1 ? l10n.minutes(1) : l10n.minutes(actualMinutes);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Stop early?'),
|
||||
title: Text(l10n.stopEarly),
|
||||
content: Text(
|
||||
"That's totally fine — you still focused for $actualMinutes ${actualMinutes == 1 ? 'minute' : 'minutes'}!",
|
||||
l10n.stopEarlyMessage(actualMinutes, minuteText),
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Keep going'),
|
||||
child: Text(l10n.keepGoing),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -184,7 +211,7 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Yes, stop'),
|
||||
child: Text(l10n.yesStop),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -210,6 +237,16 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _showDistractionSheet() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Map distraction types to translations
|
||||
final distractionOptions = [
|
||||
(type: DistractionType.phoneNotification, label: l10n.distractionPhoneNotification),
|
||||
(type: DistractionType.socialMedia, label: l10n.distractionSocialMedia),
|
||||
(type: DistractionType.thoughts, label: l10n.distractionThoughts),
|
||||
(type: DistractionType.other, label: l10n.distractionOther),
|
||||
];
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.white,
|
||||
@@ -245,9 +282,9 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Title
|
||||
const Text(
|
||||
'What pulled you away?',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.whatPulledYouAway,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -257,24 +294,24 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Distraction options
|
||||
...DistractionType.all.map((type) {
|
||||
...distractionOptions.map((option) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Text(
|
||||
DistractionType.getEmoji(type),
|
||||
DistractionType.getEmoji(option.type),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
title: Text(
|
||||
DistractionType.getDisplayName(type),
|
||||
option.label,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_recordDistraction(type);
|
||||
_recordDistraction(option.type);
|
||||
},
|
||||
),
|
||||
if (type != DistractionType.all.last)
|
||||
if (option != distractionOptions.last)
|
||||
const Divider(color: AppColors.divider),
|
||||
],
|
||||
);
|
||||
@@ -289,7 +326,7 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
Navigator.pop(context);
|
||||
_recordDistraction(null);
|
||||
},
|
||||
child: const Text('Skip this time'),
|
||||
child: Text(l10n.skipThisTime),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -302,6 +339,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _recordDistraction(String? type) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
setState(() {
|
||||
if (type != null) {
|
||||
_distractions.add(type);
|
||||
@@ -310,9 +349,9 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
|
||||
// Show encouragement toast
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("It happens. Let's gently come back."),
|
||||
duration: Duration(seconds: 2),
|
||||
SnackBar(
|
||||
content: Text(l10n.distractionEncouragement),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
@@ -326,6 +365,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
@@ -365,18 +406,18 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'I got distracted',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.iGotDistracted,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
const Text(
|
||||
'🤚',
|
||||
style: const TextStyle(fontSize: 20),
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -403,7 +444,7 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
children: [
|
||||
Icon(_isPaused ? Icons.play_arrow : Icons.pause),
|
||||
const SizedBox(width: 8),
|
||||
Text(_isPaused ? 'Resume' : 'Pause'),
|
||||
Text(_isPaused ? l10n.resume : l10n.pause),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -422,9 +463,9 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: TextButton(
|
||||
onPressed: _stopEarly,
|
||||
child: const Text(
|
||||
'Stop session',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
l10n.stopSession,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../theme/app_text_styles.dart';
|
||||
import '../models/focus_session.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
/// History Screen - Shows past focus sessions
|
||||
class HistoryScreen extends StatefulWidget {
|
||||
@@ -18,6 +19,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final sessions = _storageService.getAllSessions();
|
||||
final todayTotal = _storageService.getTodayTotalMinutes();
|
||||
final todayDistractions = _storageService.getTodayDistractionCount();
|
||||
@@ -44,16 +46,17 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Your Focus Journey'),
|
||||
title: Text(l10n.yourFocusJourney),
|
||||
backgroundColor: AppColors.background,
|
||||
),
|
||||
body: sessions.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
? _buildEmptyState(context, l10n)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
// Today's Summary Card
|
||||
_buildTodaySummary(
|
||||
l10n,
|
||||
todayTotal,
|
||||
todayDistractions,
|
||||
todayCompleted,
|
||||
@@ -64,14 +67,14 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
// Sessions by date
|
||||
...sortedDates.map((date) {
|
||||
final dateSessions = sessionsByDate[date]!;
|
||||
return _buildDateSection(date, dateSessions);
|
||||
return _buildDateSection(l10n, date, dateSessions);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
Widget _buildEmptyState(BuildContext context, AppLocalizations l10n) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
@@ -84,13 +87,13 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'No focus sessions yet',
|
||||
l10n.noFocusSessionsYet,
|
||||
style: AppTextStyles.headline,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Start your first session\nto see your progress here!',
|
||||
l10n.startFirstSession,
|
||||
style: AppTextStyles.helperText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -105,7 +108,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTodaySummary(int totalMins, int distractions, int completed) {
|
||||
Widget _buildTodaySummary(AppLocalizations l10n, int totalMins, int distractions, int completed) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
@@ -117,9 +120,9 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'📅 Today',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
'📅 ${l10n.today}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -137,7 +140,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'$completed ${completed == 1 ? 'session' : 'sessions'}',
|
||||
l10n.sessions(completed),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 14,
|
||||
@@ -152,13 +155,13 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStat('Total', '$totalMins mins', '⏱️'),
|
||||
child: _buildStat('Total', l10n.minutesValue(totalMins, l10n.minutes(totalMins)), '⏱️'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildStat(
|
||||
'Distractions',
|
||||
'$distractions ${distractions == 1 ? 'time' : 'times'}',
|
||||
l10n.distractionsCount(distractions, l10n.times(distractions)),
|
||||
'🤚',
|
||||
),
|
||||
),
|
||||
@@ -201,10 +204,10 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateSection(DateTime date, List<FocusSession> sessions) {
|
||||
Widget _buildDateSection(AppLocalizations l10n, DateTime date, List<FocusSession> sessions) {
|
||||
final isToday = _isToday(date);
|
||||
final dateLabel = isToday
|
||||
? 'Today'
|
||||
? l10n.today
|
||||
: DateFormat('EEE, MMM d').format(date);
|
||||
|
||||
final totalMinutes = sessions.fold<int>(
|
||||
@@ -231,7 +234,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'($totalMinutes mins)',
|
||||
'(${l10n.minutesValue(totalMinutes, l10n.minutes(totalMinutes))})',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 16,
|
||||
@@ -244,15 +247,15 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
),
|
||||
|
||||
// Session cards
|
||||
...sessions.map((session) => _buildSessionCard(session)),
|
||||
...sessions.map((session) => _buildSessionCard(l10n, session)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSessionCard(FocusSession session) {
|
||||
Widget _buildSessionCard(AppLocalizations l10n, FocusSession session) {
|
||||
final timeStr = DateFormat('HH:mm').format(session.startTime);
|
||||
final statusEmoji = session.completed ? '✅' : '⏸️';
|
||||
final statusText = session.completed ? 'Completed' : 'Stopped early';
|
||||
final statusText = session.completed ? l10n.completed : l10n.stoppedEarly;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@@ -286,14 +289,14 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${session.actualMinutes} ${session.actualMinutes == 1 ? 'minute' : 'minutes'}',
|
||||
l10n.minutesValue(session.actualMinutes, l10n.minutes(session.actualMinutes)),
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
if (session.distractionCount > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'🤚 ${session.distractionCount} ${session.distractionCount == 1 ? 'distraction' : 'distractions'}',
|
||||
l10n.distractionsCount(session.distractionCount, l10n.times(session.distractionCount)),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 14,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
import '../services/encouragement_service.dart';
|
||||
@@ -37,6 +38,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
@@ -47,7 +50,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
children: [
|
||||
// App Title
|
||||
Text(
|
||||
'FocusBuddy',
|
||||
l10n.appTitle,
|
||||
style: AppTextStyles.appTitle,
|
||||
),
|
||||
|
||||
@@ -64,7 +67,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
'$_defaultDuration minutes',
|
||||
l10n.minutesValue(_defaultDuration, l10n.minutes(_defaultDuration)),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 28,
|
||||
@@ -98,7 +101,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Start Focusing'),
|
||||
Text(l10n.startFocusing),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.play_arrow,
|
||||
@@ -113,7 +116,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
// Helper Text
|
||||
Text(
|
||||
"Tap 'I got distracted'\nanytime — no guilt.",
|
||||
l10n.tapDistractionAnytime,
|
||||
style: AppTextStyles.helperText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -134,7 +137,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
label: const Text('History'),
|
||||
label: Text(l10n.history),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
@@ -148,7 +151,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
_loadDefaultDuration();
|
||||
},
|
||||
icon: const Icon(Icons.settings),
|
||||
label: const Text('Settings'),
|
||||
label: Text(l10n.settings),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
import 'home_screen.dart';
|
||||
import '../services/encouragement_service.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
/// Onboarding Screen - Shows on first launch
|
||||
class OnboardingScreen extends StatefulWidget {
|
||||
@@ -35,27 +36,7 @@ class OnboardingScreen extends StatefulWidget {
|
||||
class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
|
||||
final List<OnboardingPage> _pages = [
|
||||
OnboardingPage(
|
||||
emoji: '💚',
|
||||
title: 'Focus without guilt',
|
||||
description:
|
||||
"This app is different — it won't punish you for losing focus.\n\nPerfect for ADHD, anxiety, or anyone who finds traditional timers too harsh.",
|
||||
),
|
||||
OnboardingPage(
|
||||
emoji: '🤚',
|
||||
title: 'Tap when you get distracted',
|
||||
description:
|
||||
"We'll gently remind you to come back.\n\nNo shame. No stress. Just a friendly nudge.",
|
||||
),
|
||||
OnboardingPage(
|
||||
emoji: '📊',
|
||||
title: 'Track your progress',
|
||||
description:
|
||||
"See how you're improving, one session at a time.\n\nEvery distraction is just data — not failure.",
|
||||
),
|
||||
];
|
||||
static const int _totalPages = 3;
|
||||
|
||||
void _onPageChanged(int page) {
|
||||
setState(() {
|
||||
@@ -64,7 +45,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
}
|
||||
|
||||
void _nextPage() {
|
||||
if (_currentPage < _pages.length - 1) {
|
||||
if (_currentPage < _totalPages - 1) {
|
||||
_pageController.animateToPage(
|
||||
_currentPage + 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -102,6 +83,26 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
final pages = [
|
||||
OnboardingPage(
|
||||
emoji: '💚',
|
||||
title: l10n.onboarding1Title,
|
||||
description: l10n.onboarding1Description,
|
||||
),
|
||||
OnboardingPage(
|
||||
emoji: '🤚',
|
||||
title: l10n.onboarding2Title,
|
||||
description: l10n.onboarding2Description,
|
||||
),
|
||||
OnboardingPage(
|
||||
emoji: '📊',
|
||||
title: l10n.onboarding3Title,
|
||||
description: l10n.onboarding3Description,
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
@@ -114,9 +115,9 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextButton(
|
||||
onPressed: _skipOnboarding,
|
||||
child: const Text(
|
||||
'Skip',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
l10n.skip,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -132,9 +133,9 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: _onPageChanged,
|
||||
itemCount: _pages.length,
|
||||
itemCount: pages.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildPage(_pages[index]);
|
||||
return _buildPage(pages[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -145,7 +146,7 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
_pages.length,
|
||||
pages.length,
|
||||
(index) => _buildIndicator(index == _currentPage),
|
||||
),
|
||||
),
|
||||
@@ -159,9 +160,9 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _nextPage,
|
||||
child: Text(
|
||||
_currentPage == _pages.length - 1
|
||||
? 'Get Started'
|
||||
: 'Next',
|
||||
_currentPage == pages.length - 1
|
||||
? l10n.getStarted
|
||||
: l10n.next,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
|
||||
@@ -13,7 +14,20 @@ class SettingsScreen extends StatefulWidget {
|
||||
return prefs.getInt(_durationKey) ?? 25;
|
||||
}
|
||||
|
||||
/// Get the saved locale
|
||||
static Future<String?> getSavedLocale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_localeKey);
|
||||
}
|
||||
|
||||
/// Save the locale
|
||||
static Future<void> saveLocale(String localeCode) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_localeKey, localeCode);
|
||||
}
|
||||
|
||||
static const String _durationKey = 'default_duration';
|
||||
static const String _localeKey = 'app_locale';
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
@@ -21,6 +35,7 @@ class SettingsScreen extends StatefulWidget {
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
int _selectedDuration = 25; // Default
|
||||
String _selectedLocale = 'en'; // Default
|
||||
|
||||
final List<int> _durationOptions = [15, 25, 45];
|
||||
|
||||
@@ -28,6 +43,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSavedDuration();
|
||||
_loadSavedLocale();
|
||||
}
|
||||
|
||||
Future<void> _loadSavedDuration() async {
|
||||
@@ -37,6 +53,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadSavedLocale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_selectedLocale = prefs.getString(SettingsScreen._localeKey) ?? 'en';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveDuration(int duration) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(SettingsScreen._durationKey, duration);
|
||||
@@ -45,12 +68,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveLocale(String localeCode) async {
|
||||
await SettingsScreen.saveLocale(localeCode);
|
||||
setState(() {
|
||||
_selectedLocale = localeCode;
|
||||
});
|
||||
|
||||
// Show snackbar to inform user to restart
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.onboardingReset),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
title: Text(l10n.settings),
|
||||
backgroundColor: AppColors.background,
|
||||
),
|
||||
body: ListView(
|
||||
@@ -58,31 +99,43 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
children: [
|
||||
// Focus Duration Section
|
||||
_buildSection(
|
||||
title: 'Focus Settings',
|
||||
title: l10n.focusSettings,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
'Default Focus Duration',
|
||||
l10n.defaultFocusDuration,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
),
|
||||
..._durationOptions.map((duration) {
|
||||
return _buildDurationOption(duration);
|
||||
return _buildDurationOption(l10n, duration);
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Language Section
|
||||
_buildSection(
|
||||
title: l10n.language,
|
||||
children: [
|
||||
_buildLanguageOption(l10n, 'en', l10n.english),
|
||||
const Divider(color: AppColors.divider),
|
||||
_buildLanguageOption(l10n, 'zh', l10n.chinese),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// About Section
|
||||
_buildSection(
|
||||
title: 'About',
|
||||
title: l10n.about,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
'Privacy Policy',
|
||||
l10n.privacyPolicy,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
trailing: const Icon(
|
||||
@@ -98,7 +151,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
'About FocusBuddy',
|
||||
l10n.aboutFocusBuddy,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
trailing: const Icon(
|
||||
@@ -114,7 +167,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
'Reset Onboarding',
|
||||
l10n.resetOnboarding,
|
||||
style: AppTextStyles.bodyText.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
@@ -136,7 +189,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// Version info
|
||||
Center(
|
||||
child: Text(
|
||||
'Version 1.0.0 (MVP)',
|
||||
l10n.version,
|
||||
style: AppTextStyles.helperText.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
@@ -174,7 +227,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDurationOption(int duration) {
|
||||
Widget _buildDurationOption(AppLocalizations l10n, int duration) {
|
||||
final isSelected = _selectedDuration == duration;
|
||||
|
||||
return GestureDetector(
|
||||
@@ -222,7 +275,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// Duration text
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$duration minutes',
|
||||
l10n.minutesValue(duration, l10n.minutes(duration)),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 16,
|
||||
@@ -245,9 +298,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
color: AppColors.success.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'Default',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
l10n.defaultLabel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -261,25 +314,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLanguageOption(AppLocalizations l10n, String localeCode, String label) {
|
||||
final isSelected = _selectedLocale == localeCode;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 16,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
color: isSelected ? AppColors.primary : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
color: AppColors.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () => _saveLocale(localeCode),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPrivacyPolicy() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Privacy Policy'),
|
||||
title: Text(l10n.privacyPolicyTitle),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(
|
||||
'FocusBuddy is 100% offline. We do not collect your name, email, '
|
||||
'location, or usage data. All sessions stay on your device.\n\n'
|
||||
'There is no cloud sync, no account system, and no analytics tracking.\n\n'
|
||||
'For the full privacy policy, visit:\n'
|
||||
'[Your website URL]/privacy',
|
||||
l10n.privacyPolicyContent,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
child: Text(l10n.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -287,18 +362,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
void _showAboutDialog() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('About FocusBuddy'),
|
||||
title: Text(l10n.aboutTitle),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'FocusBuddy',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.appTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -307,21 +384,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'A gentle focus timer for neurodivergent minds',
|
||||
l10n.aboutSubtitle,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'"Focus is not about never getting distracted — '
|
||||
'it\'s about gently coming back every time you do."',
|
||||
l10n.aboutQuote,
|
||||
style: AppTextStyles.encouragementQuote,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'✨ No punishment for distractions\n'
|
||||
'💚 Encouragement over criticism\n'
|
||||
'🔒 100% offline and private\n'
|
||||
'🌱 Made with care',
|
||||
l10n.aboutFeatures,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
],
|
||||
@@ -330,7 +403,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
child: Text(l10n.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -338,18 +411,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
void _resetOnboarding() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Reset Onboarding?'),
|
||||
title: Text(l10n.resetOnboardingTitle),
|
||||
content: Text(
|
||||
'This will show the onboarding screens again when you restart the app.',
|
||||
l10n.resetOnboardingMessage,
|
||||
style: AppTextStyles.bodyText,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -360,13 +435,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Onboarding reset. Restart the app to see it again.'),
|
||||
duration: Duration(seconds: 3),
|
||||
SnackBar(
|
||||
content: Text(l10n.onboardingReset),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Reset'),
|
||||
child: Text(l10n.reset),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user