多语言支持

This commit is contained in:
ytc1012
2025-11-24 11:25:33 +08:00
parent 2c6ced5c14
commit 4444c401b9
14 changed files with 672 additions and 167 deletions

View File

@@ -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,
),