多语言支持

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

3
l10n.yaml Normal file
View File

@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

219
lib/l10n/app_en.arb Normal file
View File

@@ -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)"
}

109
lib/l10n/app_zh.arb Normal file
View File

@@ -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": "中文"
}

View File

@@ -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<MyApp> {
bool _hasCompletedOnboarding = false;
bool _isLoading = true;
Locale? _locale;
@override
void initState() {
super.initState();
_checkOnboardingStatus();
_loadSavedLocale();
}
Future<void> _checkOnboardingStatus() async {
@@ -54,12 +59,32 @@ class _MyAppState extends State<MyApp> {
});
}
Future<void> _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(

View File

@@ -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<String> get all => [
scrollingSocialMedia,
gotInterrupted,
feltOverwhelmed,
justZonedOut,
phoneNotification,
socialMedia,
thoughts,
other,
];
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,6 +91,8 @@ class NotificationService {
Future<void> 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
// 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!';
: '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<void> 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<void> 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,
);
}

View File

@@ -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:

View File

@@ -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.