From 4444c401b961da819439b4a97ea740dbea700f3f Mon Sep 17 00:00:00 2001 From: ytc1012 <18001193130@163.com> Date: Mon, 24 Nov 2025 11:25:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l10n.yaml | 3 + lib/l10n/app_en.arb | 219 +++++++++++++++++++++++++ lib/l10n/app_zh.arb | 109 ++++++++++++ lib/main.dart | 25 +++ lib/models/distraction_type.dart | 47 +++--- lib/screens/complete_screen.dart | 14 +- lib/screens/focus_screen.dart | 93 ++++++++--- lib/screens/history_screen.dart | 45 ++--- lib/screens/home_screen.dart | 15 +- lib/screens/onboarding_screen.dart | 63 +++---- lib/screens/settings_screen.dart | 155 ++++++++++++----- lib/services/notification_service.dart | 35 ++-- pubspec.lock | 9 +- pubspec.yaml | 7 +- 14 files changed, 672 insertions(+), 167 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_zh.arb diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..5106eb6 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..2c1f53f --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,219 @@ +{ + "@@locale": "en", + + "appTitle": "FocusBuddy", + "@appTitle": { + "description": "The application title" + }, + + "startFocusing": "Start Focusing", + "@startFocusing": { + "description": "Button text to start a focus session" + }, + + "minutes": "{count, plural, =1{minute} other{minutes}}", + "@minutes": { + "description": "Minutes plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "minutesValue": "{count} {minutes}", + "@minutesValue": { + "description": "Minutes with value", + "placeholders": { + "count": { + "type": "int" + }, + "minutes": {} + } + }, + + "tapDistractionAnytime": "Tap 'I got distracted'\nanytime — no guilt.", + "@tapDistractionAnytime": { + "description": "Helper text on home screen" + }, + + "history": "History", + "@history": { + "description": "History navigation button" + }, + + "settings": "Settings", + "@settings": { + "description": "Settings navigation button" + }, + + "iGotDistracted": "I got distracted", + "@iGotDistracted": { + "description": "Main distraction button text" + }, + + "pause": "Pause", + "resume": "Resume", + "stopSession": "Stop session", + + "whatPulledYouAway": "What pulled you away?", + "@whatPulledYouAway": { + "description": "Distraction sheet title" + }, + + "skipThisTime": "Skip this time", + "stopEarly": "Stop early?", + "stopEarlyMessage": "That's totally fine — you still focused for {minutes} {minuteText}!", + "@stopEarlyMessage": { + "placeholders": { + "minutes": { + "type": "int" + }, + "minuteText": {} + } + }, + + "keepGoing": "Keep going", + "yesStop": "Yes, stop", + + "distractionEncouragement": "It happens. Let's gently come back.", + "@distractionEncouragement": { + "description": "Encouragement message when distracted" + }, + + "focusComplete": "Focus session complete!", + "youFocusedFor": "You focused for", + "totalToday": "Total today: {minutes} mins", + "@totalToday": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + + "distractionsCount": "Distractions: {count} {times}", + "@distractionsCount": { + "placeholders": { + "count": { + "type": "int" + }, + "times": {} + } + }, + + "times": "{count, plural, =1{time} other{times}}", + "@times": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "startAnother": "Start Another", + "viewHistory": "View History", + + "yourFocusJourney": "Your Focus Journey", + "noFocusSessionsYet": "No focus sessions yet", + "startFirstSession": "Start your first session\nto see your progress here!", + + "today": "Today", + "sessions": "{count, plural, =1{session} other{sessions}}", + "@sessions": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "completed": "Completed", + "stoppedEarly": "Stopped early", + "distractions": "{count, plural, =1{distraction} other{distractions}}", + "@distractions": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "focusSettings": "Focus Settings", + "defaultFocusDuration": "Default Focus Duration", + "defaultLabel": "Default", + + "about": "About", + "privacyPolicy": "Privacy Policy", + "aboutFocusBuddy": "About FocusBuddy", + "resetOnboarding": "Reset Onboarding", + "version": "Version 1.0.0 (MVP)", + + "privacyPolicyTitle": "Privacy Policy", + "privacyPolicyContent": "FocusBuddy is 100% offline. We do not collect your name, email, location, or usage data. All sessions stay on your device.\n\nThere is no cloud sync, no account system, and no analytics tracking.\n\nFor the full privacy policy, visit:\n[Your website URL]/privacy", + "close": "Close", + + "aboutTitle": "About FocusBuddy", + "aboutSubtitle": "A gentle focus timer for neurodivergent minds", + "aboutQuote": "\"Focus is not about never getting distracted — it's about gently coming back every time you do.\"", + "aboutFeatures": "✨ No punishment for distractions\n💚 Encouragement over criticism\n🔒 100% offline and private\n🌱 Made with care", + + "resetOnboardingTitle": "Reset Onboarding?", + "resetOnboardingMessage": "This will show the onboarding screens again when you restart the app.", + "cancel": "Cancel", + "reset": "Reset", + "onboardingReset": "Onboarding reset. Restart the app to see it again.", + + "onboarding1Title": "Focus without guilt", + "onboarding1Description": "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.", + + "onboarding2Title": "Tap when you get distracted", + "onboarding2Description": "We'll gently remind you to come back.\n\nNo shame. No stress. Just a friendly nudge.", + + "onboarding3Title": "Track your progress", + "onboarding3Description": "See how you're improving, one session at a time.\n\nEvery distraction is just data — not failure.", + + "skip": "Skip", + "next": "Next", + "getStarted": "Get Started", + + "notificationFocusInProgress": "Focus session in progress", + "notificationRemaining": "{time} remaining", + "@notificationRemaining": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + + "notificationFocusCompleteTitle": "🎉 Focus session complete!", + "notificationFocusCompleteBodyNoDistractions": "You focused for {minutes} {minuteText} without distractions!", + "@notificationFocusCompleteBodyNoDistractions": { + "placeholders": { + "minutes": { + "type": "int" + }, + "minuteText": {} + } + }, + "notificationFocusCompleteBody": "You focused for {minutes} {minuteText}. Great effort!", + "@notificationFocusCompleteBody": { + "placeholders": { + "minutes": { + "type": "int" + }, + "minuteText": {} + } + }, + + "distractionPhoneNotification": "Phone / Notification", + "distractionSocialMedia": "Social Media", + "distractionThoughts": "Thoughts / Daydream", + "distractionOther": "Other", + + "language": "Language", + "selectLanguage": "Select Language", + "english": "English", + "chinese": "中文 (Chinese)" +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..a0992c2 --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,109 @@ +{ + "@@locale": "zh", + + "appTitle": "专注伙伴", + + "startFocusing": "开始专注", + + "minutes": "{count, plural, =1{分钟} other{分钟}}", + "minutesValue": "{count} {minutes}", + + "tapDistractionAnytime": "随时点击'我分心了'\n——没有负罪感。", + + "history": "历史", + "settings": "设置", + + "iGotDistracted": "我分心了", + + "pause": "暂停", + "resume": "继续", + "stopSession": "停止会话", + + "whatPulledYouAway": "是什么分散了你的注意力?", + + "skipThisTime": "跳过", + "stopEarly": "提前停止?", + "stopEarlyMessage": "完全没问题——你已经专注了 {minutes} {minuteText}!", + + "keepGoing": "继续", + "yesStop": "确定停止", + + "distractionEncouragement": "没关系,让我们温柔地回到正轨。", + + "focusComplete": "专注完成!", + "youFocusedFor": "你专注了", + "totalToday": "今日总计:{minutes} 分钟", + + "distractionsCount": "分心:{count} {times}", + + "times": "{count, plural, =1{次} other{次}}", + + "startAnother": "再来一次", + "viewHistory": "查看历史", + + "yourFocusJourney": "你的专注之旅", + "noFocusSessionsYet": "还没有专注记录", + "startFirstSession": "开始你的第一次专注\n在这里查看进度!", + + "today": "今天", + "sessions": "{count, plural, =1{次会话} other{次会话}}", + + "completed": "已完成", + "stoppedEarly": "提前停止", + "distractions": "{count, plural, =1{次分心} other{次分心}}", + + "focusSettings": "专注设置", + "defaultFocusDuration": "默认专注时长", + "defaultLabel": "默认", + + "about": "关于", + "privacyPolicy": "隐私政策", + "aboutFocusBuddy": "关于专注伙伴", + "resetOnboarding": "重置引导", + "version": "版本 1.0.0 (MVP)", + + "privacyPolicyTitle": "隐私政策", + "privacyPolicyContent": "专注伙伴 100% 离线运行。我们不收集您的姓名、电子邮件、位置或使用数据。所有会话数据都保存在您的设备上。\n\n没有云同步,没有账户系统,没有分析追踪。\n\n完整隐私政策请访问:\n[您的网站 URL]/privacy", + "close": "关闭", + + "aboutTitle": "关于专注伙伴", + "aboutSubtitle": "为神经多样性人群设计的温柔专注计时器", + "aboutQuote": "\"专注不是永不分心——而是每次分心后温柔地回来。\"", + "aboutFeatures": "✨ 不惩罚分心\n💚 鼓励而非批评\n🔒 100% 离线和私密\n🌱 用心制作", + + "resetOnboardingTitle": "重置引导?", + "resetOnboardingMessage": "重启应用后将再次显示引导页面。", + "cancel": "取消", + "reset": "重置", + "onboardingReset": "引导已重置。重启应用后将再次显示。", + + "onboarding1Title": "无负罪感地专注", + "onboarding1Description": "这个应用与众不同——它不会因为你失去专注而惩罚你。\n\n完美适合 ADHD、焦虑症患者,或任何觉得传统计时器太苛刻的人。", + + "onboarding2Title": "分心时轻触按钮", + "onboarding2Description": "我们会温柔地提醒你回来。\n\n没有羞愧。没有压力。只是友好的提醒。", + + "onboarding3Title": "追踪你的进步", + "onboarding3Description": "看看你是如何一次次进步的。\n\n每次分心都只是数据——而非失败。", + + "skip": "跳过", + "next": "下一步", + "getStarted": "开始使用", + + "notificationFocusInProgress": "专注进行中", + "notificationRemaining": "剩余 {time}", + + "notificationFocusCompleteTitle": "🎉 专注完成!", + "notificationFocusCompleteBodyNoDistractions": "你专注了 {minutes} {minuteText},没有分心!", + "notificationFocusCompleteBody": "你专注了 {minutes} {minuteText}。做得很棒!", + + "distractionPhoneNotification": "手机/通知", + "distractionSocialMedia": "社交媒体", + "distractionThoughts": "思绪/白日梦", + "distractionOther": "其他", + + "language": "语言", + "selectLanguage": "选择语言", + "english": "English", + "chinese": "中文" +} diff --git a/lib/main.dart b/lib/main.dart index 7404b51..7f2c111 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'l10n/app_localizations.dart'; import 'theme/app_theme.dart'; import 'services/storage_service.dart'; import 'services/encouragement_service.dart'; import 'services/notification_service.dart'; import 'screens/home_screen.dart'; import 'screens/onboarding_screen.dart'; +import 'screens/settings_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -39,11 +42,13 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { bool _hasCompletedOnboarding = false; bool _isLoading = true; + Locale? _locale; @override void initState() { super.initState(); _checkOnboardingStatus(); + _loadSavedLocale(); } Future _checkOnboardingStatus() async { @@ -54,12 +59,32 @@ class _MyAppState extends State { }); } + Future _loadSavedLocale() async { + final savedLocale = await SettingsScreen.getSavedLocale(); + if (savedLocale != null) { + setState(() { + _locale = Locale(savedLocale); + }); + } + } + @override Widget build(BuildContext context) { return MaterialApp( title: 'FocusBuddy', debugShowCheckedModeBanner: false, theme: AppTheme.lightTheme, + locale: _locale, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), + Locale('zh'), + ], home: _isLoading ? const Scaffold( body: Center( diff --git a/lib/models/distraction_type.dart b/lib/models/distraction_type.dart index 4aef46c..60b09b2 100644 --- a/lib/models/distraction_type.dart +++ b/lib/models/distraction_type.dart @@ -1,21 +1,22 @@ /// Predefined distraction types class DistractionType { - static const scrollingSocialMedia = 'scrolling_social_media'; - static const gotInterrupted = 'got_interrupted'; - static const feltOverwhelmed = 'felt_overwhelmed'; - static const justZonedOut = 'just_zoned_out'; + static const phoneNotification = 'phone_notification'; + static const socialMedia = 'social_media'; + static const thoughts = 'thoughts'; + static const other = 'other'; - /// Get display name for a distraction type + /// Get display name for a distraction type (returns key for localization) static String getDisplayName(String type) { + // Returns the localization key - actual translation done in UI switch (type) { - case scrollingSocialMedia: - return 'Scrolling social media'; - case gotInterrupted: - return 'Got interrupted'; - case feltOverwhelmed: - return 'Felt overwhelmed'; - case justZonedOut: - return 'Just zoned out'; + case phoneNotification: + return 'Phone / Notification'; + case socialMedia: + return 'Social Media'; + case thoughts: + return 'Thoughts / Daydream'; + case other: + return 'Other'; default: return 'Unknown'; } @@ -24,14 +25,14 @@ class DistractionType { /// Get emoji for a distraction type static String getEmoji(String type) { switch (type) { - case scrollingSocialMedia: + case phoneNotification: return '📱'; - case gotInterrupted: - return '👥'; - case feltOverwhelmed: - return '😰'; - case justZonedOut: + case socialMedia: + return '💬'; + case thoughts: return '💭'; + case other: + return '🌟'; default: return '❓'; } @@ -39,9 +40,9 @@ class DistractionType { /// Get all distraction types static List get all => [ - scrollingSocialMedia, - gotInterrupted, - feltOverwhelmed, - justZonedOut, + phoneNotification, + socialMedia, + thoughts, + other, ]; } diff --git a/lib/screens/complete_screen.dart b/lib/screens/complete_screen.dart index 0eae893..1219471 100644 --- a/lib/screens/complete_screen.dart +++ b/lib/screens/complete_screen.dart @@ -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), ), ], ), diff --git a/lib/screens/focus_screen.dart b/lib/screens/focus_screen.dart index 9072235..b27426d 100644 --- a/lib/screens/focus_screen.dart +++ b/lib/screens/focus_screen.dart @@ -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 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 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 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 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 with WidgetsBindingObserver { ), ); }, - child: const Text('Yes, stop'), + child: Text(l10n.yesStop), ), ], ), @@ -210,6 +237,16 @@ class _FocusScreenState extends State 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 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 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 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 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 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 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 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 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 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, ), diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index 12be93f..75b7461 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -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 { @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 { 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 { // 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 { ), 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 { ); } - 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 { 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 { 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 { 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 { ); } - Widget _buildDateSection(DateTime date, List sessions) { + Widget _buildDateSection(AppLocalizations l10n, DateTime date, List sessions) { final isToday = _isToday(date); final dateLabel = isToday - ? 'Today' + ? l10n.today : DateFormat('EEE, MMM d').format(date); final totalMinutes = sessions.fold( @@ -231,7 +234,7 @@ class _HistoryScreenState extends State { ), 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 { ), // 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 { 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, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 00465f4..d0c875f 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -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 { @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 { children: [ // App Title Text( - 'FocusBuddy', + l10n.appTitle, style: AppTextStyles.appTitle, ), @@ -64,7 +67,7 @@ class _HomeScreenState extends State { 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 { 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 { // 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 { ); }, 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 { _loadDefaultDuration(); }, icon: const Icon(Icons.settings), - label: const Text('Settings'), + label: Text(l10n.settings), ), ], ), diff --git a/lib/screens/onboarding_screen.dart b/lib/screens/onboarding_screen.dart index bedd20e..b0f24a9 100644 --- a/lib/screens/onboarding_screen.dart +++ b/lib/screens/onboarding_screen.dart @@ -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 { final PageController _pageController = PageController(); int _currentPage = 0; - - final List _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 { } 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 { @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 { 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 { 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 { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( - _pages.length, + pages.length, (index) => _buildIndicator(index == _currentPage), ), ), @@ -159,9 +160,9 @@ class _OnboardingScreenState extends State { child: ElevatedButton( onPressed: _nextPage, child: Text( - _currentPage == _pages.length - 1 - ? 'Get Started' - : 'Next', + _currentPage == pages.length - 1 + ? l10n.getStarted + : l10n.next, ), ), ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 9d83c9c..a9c0de5 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -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 getSavedLocale() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_localeKey); + } + + /// Save the locale + static Future 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 createState() => _SettingsScreenState(); @@ -21,6 +35,7 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { int _selectedDuration = 25; // Default + String _selectedLocale = 'en'; // Default final List _durationOptions = [15, 25, 45]; @@ -28,6 +43,7 @@ class _SettingsScreenState extends State { void initState() { super.initState(); _loadSavedDuration(); + _loadSavedLocale(); } Future _loadSavedDuration() async { @@ -37,6 +53,13 @@ class _SettingsScreenState extends State { }); } + Future _loadSavedLocale() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedLocale = prefs.getString(SettingsScreen._localeKey) ?? 'en'; + }); + } + Future _saveDuration(int duration) async { final prefs = await SharedPreferences.getInstance(); await prefs.setInt(SettingsScreen._durationKey, duration); @@ -45,12 +68,30 @@ class _SettingsScreenState extends State { }); } + Future _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 { 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 { ListTile( contentPadding: EdgeInsets.zero, title: Text( - 'About FocusBuddy', + l10n.aboutFocusBuddy, style: AppTextStyles.bodyText, ), trailing: const Icon( @@ -114,7 +167,7 @@ class _SettingsScreenState extends State { 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 { // 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 { ); } - Widget _buildDurationOption(int duration) { + Widget _buildDurationOption(AppLocalizations l10n, int duration) { final isSelected = _selectedDuration == duration; return GestureDetector( @@ -222,7 +275,7 @@ class _SettingsScreenState extends State { // 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 { 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 { ); } + 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 { } 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 { ), 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 { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Close'), + child: Text(l10n.close), ), ], ), @@ -338,18 +411,20 @@ class _SettingsScreenState extends State { } 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 { 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), ), ], ), diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index caa355a..d5a8370 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -91,6 +91,8 @@ class NotificationService { Future showFocusCompletedNotification({ required int minutes, required int distractionCount, + String? title, + String? body, }) async { if (kIsWeb || !_initialized) return; @@ -116,22 +118,23 @@ class NotificationService { iOS: iosDetails, ); - // Create notification message - final title = '🎉 Focus session complete!'; - final body = distractionCount == 0 - ? 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'} without distractions!' - : 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'}. Great effort!'; + // Use provided title/body or fall back to English + final notificationTitle = title ?? '🎉 Focus session complete!'; + final notificationBody = body ?? + (distractionCount == 0 + ? 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'} without distractions!' + : 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'}. Great effort!'); await _notifications.show( 0, // Notification ID - title, - body, + notificationTitle, + notificationBody, notificationDetails, payload: 'focus_completed', ); if (kDebugMode) { - print('Notification shown: $title - $body'); + print('Notification shown: $notificationTitle - $notificationBody'); } } catch (e) { if (kDebugMode) { @@ -207,11 +210,13 @@ class NotificationService { Future showOngoingFocusNotification({ required int remainingMinutes, required int remainingSeconds, + String? title, + String? timeRemainingText, }) async { if (kIsWeb || !_initialized) return; try { - // Format time display + // Format time display for fallback final timeStr = '${remainingMinutes.toString().padLeft(2, '0')}:${(remainingSeconds % 60).toString().padLeft(2, '0')}'; const androidDetails = AndroidNotificationDetails( @@ -240,10 +245,14 @@ class NotificationService { iOS: iosDetails, ); + // Use provided text or fall back to English + final notificationTitle = title ?? '⏱️ Focus session in progress'; + final notificationBody = timeRemainingText ?? '$timeStr remaining'; + await _notifications.show( 2, // Use ID 2 for ongoing notifications - '⏱️ Focus session in progress', - '$timeStr remaining', + notificationTitle, + notificationBody, notificationDetails, payload: 'focus_ongoing', ); @@ -258,11 +267,15 @@ class NotificationService { Future updateOngoingFocusNotification({ required int remainingMinutes, required int remainingSeconds, + String? title, + String? timeRemainingText, }) async { // On Android, showing the same notification ID updates it await showOngoingFocusNotification( remainingMinutes: remainingMinutes, remainingSeconds: remainingSeconds, + title: title, + timeRemainingText: timeRemainingText, ); } diff --git a/pubspec.lock b/pubspec.lock index 3a22b38..d6be813 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -254,6 +254,11 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.2.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -348,10 +353,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.flutter-io.cn" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2b99ce5..135c3b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -41,7 +43,7 @@ dependencies: flutter_local_notifications: ^17.0.0 # Notifications path_provider: ^2.1.0 # File paths shared_preferences: ^2.2.0 # Simple key-value storage (for onboarding) - intl: ^0.19.0 # Date formatting + intl: ^0.20.2 # Date formatting and i18n google_fonts: ^6.1.0 # Google Fonts (Nunito) dev_dependencies: @@ -63,6 +65,9 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + # Enable code generation for localization + generate: true + # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class.