diff --git a/lib/src/application/focus_seed_controller.dart b/lib/src/application/focus_seed_controller.dart index 2a35e5d..9903f37 100644 --- a/lib/src/application/focus_seed_controller.dart +++ b/lib/src/application/focus_seed_controller.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -168,22 +166,41 @@ class FocusSeedController extends ChangeNotifier { return _commit(current.copyWith(reflections: reflections)); } - Future importState(String raw) async { - try { - final decoded = jsonDecode(raw); - if (decoded is! Map) { - return false; - } - return _commit(AppState.fromJson(decoded)); - } on Object { - return false; - } - } - Future clearData() async { return _commit(AppState.starter(defaultHabitTitles: _defaultHabitTitles)); } + Future clearToday() async { + final current = _state; + if (current == null) { + return false; + } + final today = todayKey(); + final habits = current.habits + .map( + (habit) => habit.copyWith( + completedDates: Set.from(habit.completedDates) + ..remove(today), + ), + ) + .toList(growable: false); + final sessions = current.sessions + .where((session) => dateKey(session.completedAt) != today) + .toList(growable: false); + final reflections = current.reflections + .where((reflection) => reflection.dateKey != today) + .toList(growable: false); + _analytics.track('today_cleared'); + return _commit( + current.copyWith( + anchor: current.anchorDateKey == today ? '' : current.anchor, + habits: habits, + sessions: sessions, + reflections: reflections, + ), + ); + } + Future _commit(AppState nextState) async { final previousState = _state; _state = nextState; diff --git a/lib/src/features/me/me_screen.dart b/lib/src/features/me/me_screen.dart index debaa46..f062487 100644 --- a/lib/src/features/me/me_screen.dart +++ b/lib/src/features/me/me_screen.dart @@ -113,35 +113,29 @@ class MeScreen extends StatelessWidget { ], const SizedBox(height: 18), Text( - l10n.yourData, + l10n.localData, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), + _DataSnapshotCard( + focusSessions: state.sessions.length, + habits: state.habits.length, + reflections: state.reflections.length, + ), + const SizedBox(height: 12), SurfacePanel( padding: const EdgeInsets.symmetric(horizontal: 14), child: Column( children: [ SettingsTile( - icon: Icons.ios_share_outlined, - title: l10n.exportJson, - subtitle: l10n.exportJsonSubtitle, - onTap: () => showExportDialog(context, state), - ), - const Divider(), - SettingsTile( - icon: Icons.file_upload_outlined, - title: l10n.importJson, - subtitle: l10n.importJsonSubtitle, + icon: Icons.cleaning_services_outlined, + title: l10n.clearToday, + subtitle: l10n.clearTodaySubtitle, onTap: - () => showImportDialog(context, (raw) async { - final imported = await controller.importState(raw); - if (context.mounted) { - showSnack( - context, - imported - ? l10n.dataImported - : l10n.importFailed, - ); + () => confirmClearToday(context, () async { + final cleared = await controller.clearToday(); + if (context.mounted && cleared) { + showSnack(context, l10n.todayDataCleared); } }), ), @@ -159,9 +153,9 @@ class MeScreen extends StatelessWidget { SurfacePanel( padding: const EdgeInsets.symmetric(horizontal: 14), child: SettingsTile( - icon: Icons.delete_outline, - title: l10n.resetLocalGarden, - subtitle: l10n.resetLocalGardenSubtitle, + icon: Icons.delete_forever_outlined, + title: l10n.deleteAllLocalData, + subtitle: l10n.deleteAllLocalDataSubtitle, destructive: true, onTap: () => confirmClearData(context, () async { @@ -188,6 +182,96 @@ class MeScreen extends StatelessWidget { } } +class _DataSnapshotCard extends StatelessWidget { + const _DataSnapshotCard({ + required this.focusSessions, + required this.habits, + required this.reflections, + }); + + final int focusSessions; + final int habits; + final int reflections; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return SurfacePanel( + elevated: true, + child: Row( + children: [ + Expanded( + child: _DataMetric( + icon: Icons.timer_outlined, + label: l10n.focusDataLabel, + value: focusSessions, + ), + ), + Expanded( + child: _DataMetric( + icon: Icons.checklist_rounded, + label: l10n.habitsDataLabel, + value: habits, + ), + ), + Expanded( + child: _DataMetric( + icon: Icons.edit_note_rounded, + label: l10n.notesDataLabel, + value: reflections, + ), + ), + ], + ), + ); + } +} + +class _DataMetric extends StatelessWidget { + const _DataMetric({ + required this.icon, + required this.label, + required this.value, + }); + + final IconData icon; + final String label; + final int value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scheme = theme.colorScheme; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: scheme.primary), + const SizedBox(height: 8), + TweenAnimationBuilder( + tween: Tween(end: value.toDouble()), + duration: const Duration(milliseconds: 260), + curve: Curves.easeOutCubic, + builder: + (context, animatedValue, _) => Text( + animatedValue.round().toString(), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + ), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ); + } +} + class _SectionHeader extends StatelessWidget { const _SectionHeader({required this.title, required this.detail}); diff --git a/lib/src/l10n/app_en.arb b/lib/src/l10n/app_en.arb index 831ef85..982ded5 100644 --- a/lib/src/l10n/app_en.arb +++ b/lib/src/l10n/app_en.arb @@ -114,19 +114,17 @@ "intentionHint": "What matters most today?", "deleteHabitTitle": "Delete habit?", "deleteHabitBody": "Past completion records for this habit will be removed.", - "exportJson": "Export JSON", "close": "Close", "copy": "Copy", - "exportCopied": "Export copied", - "importJson": "Import JSON", - "importHint": "Paste Focus Seed JSON export", - "import": "Import", - "clearDataTitle": "Clear local data?", - "clearDataBody": "This resets habits, focus sessions, and reflections on this device.", + "clearTodayDataTitle": "Clear today?", + "clearTodayDataBody": "This removes today's intention, habit checks, focus sessions, and reflection. Past days stay untouched.", + "clearAllDataTitle": "Delete all local data?", + "clearAllDataBody": "This deletes intentions, habits, focus sessions, and reflections on this device. It cannot be undone.", "clear": "Clear", + "deleteAll": "Delete all", "privacyTitle": "Privacy", "privacyBodyOne": "Focus Seed stores your intention, habits, focus sessions, reflections, and settings locally on this device. This version has no sign-in, cloud sync, ads, analytics SDK, or billing SDK.", - "privacyBodyTwo": "You can export, import, or clear local data from Me.", + "privacyBodyTwo": "You can clear today's progress or delete all local data from Me at any time.", "reflectionEmptyError": "Write one honest sentence.", "reflectionSaveError": "Could not save. Please try again.", "reflectionSheetTitle": "How did it go?", @@ -149,15 +147,17 @@ "languageDefault": "Default", "languageEnglish": "English", "languageChinese": "Chinese", - "yourData": "Your data", - "exportJsonSubtitle": "Copy a readable local backup.", - "importJsonSubtitle": "Restore a previous Focus Seed export.", + "localData": "Local data", + "focusDataLabel": "Focus", + "habitsDataLabel": "Habits", + "notesDataLabel": "Notes", + "clearToday": "Clear today", + "clearTodaySubtitle": "Remove only today's progress.", "privacySubtitle": "Everything stays on this device.", - "resetLocalGarden": "Reset local garden", - "resetLocalGardenSubtitle": "Clear intentions, habits, sessions, and notes.", - "dataImported": "Data imported", - "importFailed": "Import failed", - "localDataCleared": "Local data cleared", + "deleteAllLocalData": "Delete all local data", + "deleteAllLocalDataSubtitle": "Reset Focus Seed on this device.", + "todayDataCleared": "Today's data cleared", + "localDataCleared": "Local data deleted", "journeyTitle": "Today's journey", "recentFocus": "Recent focus", "sessionsTotal": "{count} sessions total", diff --git a/lib/src/l10n/app_zh.arb b/lib/src/l10n/app_zh.arb index 3851373..d34a242 100644 --- a/lib/src/l10n/app_zh.arb +++ b/lib/src/l10n/app_zh.arb @@ -114,19 +114,17 @@ "intentionHint": "\u4eca\u5929\u6700\u91cd\u8981\u7684\u662f\u4ec0\u4e48\uff1f", "deleteHabitTitle": "\u5220\u9664\u4e60\u60ef\uff1f", "deleteHabitBody": "\u8fd9\u4e2a\u4e60\u60ef\u7684\u5386\u53f2\u5b8c\u6210\u8bb0\u5f55\u4e5f\u4f1a\u5220\u9664\u3002", - "exportJson": "\u5bfc\u51fa JSON", "close": "\u5173\u95ed", "copy": "\u590d\u5236", - "exportCopied": "\u5df2\u590d\u5236\u5bfc\u51fa\u5185\u5bb9", - "importJson": "\u5bfc\u5165 JSON", - "importHint": "\u7c98\u8d34 Focus Seed \u7684 JSON \u5bfc\u51fa\u5185\u5bb9", - "import": "\u5bfc\u5165", - "clearDataTitle": "\u6e05\u9664\u672c\u5730\u6570\u636e\uff1f", - "clearDataBody": "\u4f1a\u6e05\u9664\u672c\u673a\u7684\u76ee\u6807\u3001\u4e60\u60ef\u3001\u4e13\u6ce8\u8bb0\u5f55\u548c\u590d\u76d8\u3002", + "clearTodayDataTitle": "清理今日数据?", + "clearTodayDataBody": "只清除今天的目标、打卡、专注记录和复盘,过去日期的数据会保留。", + "clearAllDataTitle": "删除全部本地数据?", + "clearAllDataBody": "会删除本机的目标、习惯、专注记录和复盘,无法撤销。", "clear": "\u6e05\u9664", + "deleteAll": "全部删除", "privacyTitle": "\u9690\u79c1", "privacyBodyOne": "Focus Seed \u53ea\u5728\u672c\u673a\u4fdd\u5b58\u76ee\u6807\u3001\u4e60\u60ef\u3001\u4e13\u6ce8\u8bb0\u5f55\u3001\u590d\u76d8\u548c\u8bbe\u7f6e\u3002\u5f53\u524d\u7248\u672c\u6ca1\u6709\u767b\u5f55\u3001\u4e91\u540c\u6b65\u3001\u5e7f\u544a\u3001\u5206\u6790 SDK \u6216\u4ed8\u8d39 SDK\u3002", - "privacyBodyTwo": "\u4f60\u53ef\u4ee5\u5728\u201c\u6211\u7684\u7a7a\u95f4\u201d\u5bfc\u51fa\u3001\u5bfc\u5165\u6216\u6e05\u9664\u672c\u5730\u6570\u636e\u3002", + "privacyBodyTwo": "你可以随时清理今日进度,或删除本机的全部本地数据。", "reflectionEmptyError": "\u5199\u4e00\u53e5\u771f\u5b9e\u7684\u603b\u7ed3\u3002", "reflectionSaveError": "\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002", "reflectionSheetTitle": "\u590d\u76d8\u4e00\u4e0b", @@ -149,15 +147,17 @@ "languageDefault": "\u9ed8\u8ba4", "languageEnglish": "English", "languageChinese": "\u4e2d\u6587", - "yourData": "\u6570\u636e", - "exportJsonSubtitle": "\u590d\u5236\u4e00\u4efd\u672c\u5730\u5907\u4efd\u3002", - "importJsonSubtitle": "\u6062\u590d\u4e4b\u524d\u5bfc\u51fa\u7684\u6570\u636e\u3002", + "localData": "本地数据", + "focusDataLabel": "专注", + "habitsDataLabel": "习惯", + "notesDataLabel": "记录", + "clearToday": "清理今日", + "clearTodaySubtitle": "只移除今天的进度。", "privacySubtitle": "\u6570\u636e\u53ea\u4fdd\u5b58\u5728\u672c\u673a\u3002", - "resetLocalGarden": "\u6e05\u9664\u672c\u5730\u6570\u636e", - "resetLocalGardenSubtitle": "\u6e05\u9664\u76ee\u6807\u3001\u4e60\u60ef\u3001\u8bb0\u5f55\u548c\u590d\u76d8\u3002", - "dataImported": "\u5df2\u5bfc\u5165\u6570\u636e", - "importFailed": "\u5bfc\u5165\u5931\u8d25", - "localDataCleared": "\u672c\u5730\u6570\u636e\u5df2\u6e05\u9664", + "deleteAllLocalData": "删除全部本地数据", + "deleteAllLocalDataSubtitle": "重置这台设备上的 Focus Seed。", + "todayDataCleared": "今日数据已清理", + "localDataCleared": "本地数据已删除", "journeyTitle": "\u4eca\u65e5\u8fdb\u5c55", "recentFocus": "\u6700\u8fd1\u4e13\u6ce8", "sessionsTotal": "\u5171 {count} \u6b21", diff --git a/lib/src/l10n/generated/app_localizations.dart b/lib/src/l10n/generated/app_localizations.dart index 59936fa..28cc1f3 100644 --- a/lib/src/l10n/generated/app_localizations.dart +++ b/lib/src/l10n/generated/app_localizations.dart @@ -62,7 +62,8 @@ import 'app_localizations_zh.dart'; /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { - AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -70,7 +71,8 @@ abstract class AppLocalizations { return Localizations.of(context, AppLocalizations)!; } - static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -82,17 +84,18 @@ abstract class AppLocalizations { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('en'), - Locale('zh') + Locale('zh'), ]; /// No description provided for @appTitle. @@ -491,12 +494,6 @@ abstract class AppLocalizations { /// **'Past completion records for this habit will be removed.'** String get deleteHabitBody; - /// No description provided for @exportJson. - /// - /// In en, this message translates to: - /// **'Export JSON'** - String get exportJson; - /// No description provided for @close. /// /// In en, this message translates to: @@ -509,41 +506,29 @@ abstract class AppLocalizations { /// **'Copy'** String get copy; - /// No description provided for @exportCopied. + /// No description provided for @clearTodayDataTitle. /// /// In en, this message translates to: - /// **'Export copied'** - String get exportCopied; + /// **'Clear today?'** + String get clearTodayDataTitle; - /// No description provided for @importJson. + /// No description provided for @clearTodayDataBody. /// /// In en, this message translates to: - /// **'Import JSON'** - String get importJson; + /// **'This removes today\'s intention, habit checks, focus sessions, and reflection. Past days stay untouched.'** + String get clearTodayDataBody; - /// No description provided for @importHint. + /// No description provided for @clearAllDataTitle. /// /// In en, this message translates to: - /// **'Paste Focus Seed JSON export'** - String get importHint; + /// **'Delete all local data?'** + String get clearAllDataTitle; - /// No description provided for @import. + /// No description provided for @clearAllDataBody. /// /// In en, this message translates to: - /// **'Import'** - String get import; - - /// No description provided for @clearDataTitle. - /// - /// In en, this message translates to: - /// **'Clear local data?'** - String get clearDataTitle; - - /// No description provided for @clearDataBody. - /// - /// In en, this message translates to: - /// **'This resets habits, focus sessions, and reflections on this device.'** - String get clearDataBody; + /// **'This deletes intentions, habits, focus sessions, and reflections on this device. It cannot be undone.'** + String get clearAllDataBody; /// No description provided for @clear. /// @@ -551,6 +536,12 @@ abstract class AppLocalizations { /// **'Clear'** String get clear; + /// No description provided for @deleteAll. + /// + /// In en, this message translates to: + /// **'Delete all'** + String get deleteAll; + /// No description provided for @privacyTitle. /// /// In en, this message translates to: @@ -566,7 +557,7 @@ abstract class AppLocalizations { /// No description provided for @privacyBodyTwo. /// /// In en, this message translates to: - /// **'You can export, import, or clear local data from Me.'** + /// **'You can clear today\'s progress or delete all local data from Me at any time.'** String get privacyBodyTwo; /// No description provided for @reflectionEmptyError. @@ -701,23 +692,41 @@ abstract class AppLocalizations { /// **'Chinese'** String get languageChinese; - /// No description provided for @yourData. + /// No description provided for @localData. /// /// In en, this message translates to: - /// **'Your data'** - String get yourData; + /// **'Local data'** + String get localData; - /// No description provided for @exportJsonSubtitle. + /// No description provided for @focusDataLabel. /// /// In en, this message translates to: - /// **'Copy a readable local backup.'** - String get exportJsonSubtitle; + /// **'Focus'** + String get focusDataLabel; - /// No description provided for @importJsonSubtitle. + /// No description provided for @habitsDataLabel. /// /// In en, this message translates to: - /// **'Restore a previous Focus Seed export.'** - String get importJsonSubtitle; + /// **'Habits'** + String get habitsDataLabel; + + /// No description provided for @notesDataLabel. + /// + /// In en, this message translates to: + /// **'Notes'** + String get notesDataLabel; + + /// No description provided for @clearToday. + /// + /// In en, this message translates to: + /// **'Clear today'** + String get clearToday; + + /// No description provided for @clearTodaySubtitle. + /// + /// In en, this message translates to: + /// **'Remove only today\'s progress.'** + String get clearTodaySubtitle; /// No description provided for @privacySubtitle. /// @@ -725,34 +734,28 @@ abstract class AppLocalizations { /// **'Everything stays on this device.'** String get privacySubtitle; - /// No description provided for @resetLocalGarden. + /// No description provided for @deleteAllLocalData. /// /// In en, this message translates to: - /// **'Reset local garden'** - String get resetLocalGarden; + /// **'Delete all local data'** + String get deleteAllLocalData; - /// No description provided for @resetLocalGardenSubtitle. + /// No description provided for @deleteAllLocalDataSubtitle. /// /// In en, this message translates to: - /// **'Clear intentions, habits, sessions, and notes.'** - String get resetLocalGardenSubtitle; + /// **'Reset Focus Seed on this device.'** + String get deleteAllLocalDataSubtitle; - /// No description provided for @dataImported. + /// No description provided for @todayDataCleared. /// /// In en, this message translates to: - /// **'Data imported'** - String get dataImported; - - /// No description provided for @importFailed. - /// - /// In en, this message translates to: - /// **'Import failed'** - String get importFailed; + /// **'Today\'s data cleared'** + String get todayDataCleared; /// No description provided for @localDataCleared. /// /// In en, this message translates to: - /// **'Local data cleared'** + /// **'Local data deleted'** String get localDataCleared; /// No description provided for @journeyTitle. @@ -924,7 +927,8 @@ abstract class AppLocalizations { String get defaultHabitReflect; } -class _AppLocalizationsDelegate extends LocalizationsDelegate { +class _AppLocalizationsDelegate + extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override @@ -933,25 +937,26 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate } @override - bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode); + bool isSupported(Locale locale) => + ['en', 'zh'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'en': return AppLocalizationsEn(); - case 'zh': return AppLocalizationsZh(); + case 'en': + return AppLocalizationsEn(); + case 'zh': + return AppLocalizationsZh(); } throw FlutterError( 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'an issue with the localizations generation tool. Please file an issue ' 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' + 'that was used.', ); } diff --git a/lib/src/l10n/generated/app_localizations_en.dart b/lib/src/l10n/generated/app_localizations_en.dart index fa7f48c..31a81ee 100644 --- a/lib/src/l10n/generated/app_localizations_en.dart +++ b/lib/src/l10n/generated/app_localizations_en.dart @@ -216,10 +216,8 @@ class AppLocalizationsEn extends AppLocalizations { String get deleteHabitTitle => 'Delete habit?'; @override - String get deleteHabitBody => 'Past completion records for this habit will be removed.'; - - @override - String get exportJson => 'Export JSON'; + String get deleteHabitBody => + 'Past completion records for this habit will be removed.'; @override String get close => 'Close'; @@ -228,34 +226,35 @@ class AppLocalizationsEn extends AppLocalizations { String get copy => 'Copy'; @override - String get exportCopied => 'Export copied'; + String get clearTodayDataTitle => 'Clear today?'; @override - String get importJson => 'Import JSON'; + String get clearTodayDataBody => + 'This removes today\'s intention, habit checks, focus sessions, and reflection. Past days stay untouched.'; @override - String get importHint => 'Paste Focus Seed JSON export'; + String get clearAllDataTitle => 'Delete all local data?'; @override - String get import => 'Import'; - - @override - String get clearDataTitle => 'Clear local data?'; - - @override - String get clearDataBody => 'This resets habits, focus sessions, and reflections on this device.'; + String get clearAllDataBody => + 'This deletes intentions, habits, focus sessions, and reflections on this device. It cannot be undone.'; @override String get clear => 'Clear'; + @override + String get deleteAll => 'Delete all'; + @override String get privacyTitle => 'Privacy'; @override - String get privacyBodyOne => 'Focus Seed stores your intention, habits, focus sessions, reflections, and settings locally on this device. This version has no sign-in, cloud sync, ads, analytics SDK, or billing SDK.'; + String get privacyBodyOne => + 'Focus Seed stores your intention, habits, focus sessions, reflections, and settings locally on this device. This version has no sign-in, cloud sync, ads, analytics SDK, or billing SDK.'; @override - String get privacyBodyTwo => 'You can export, import, or clear local data from Me.'; + String get privacyBodyTwo => + 'You can clear today\'s progress or delete all local data from Me at any time.'; @override String get reflectionEmptyError => 'Write one honest sentence.'; @@ -324,31 +323,37 @@ class AppLocalizationsEn extends AppLocalizations { String get languageChinese => 'Chinese'; @override - String get yourData => 'Your data'; + String get localData => 'Local data'; @override - String get exportJsonSubtitle => 'Copy a readable local backup.'; + String get focusDataLabel => 'Focus'; @override - String get importJsonSubtitle => 'Restore a previous Focus Seed export.'; + String get habitsDataLabel => 'Habits'; + + @override + String get notesDataLabel => 'Notes'; + + @override + String get clearToday => 'Clear today'; + + @override + String get clearTodaySubtitle => 'Remove only today\'s progress.'; @override String get privacySubtitle => 'Everything stays on this device.'; @override - String get resetLocalGarden => 'Reset local garden'; + String get deleteAllLocalData => 'Delete all local data'; @override - String get resetLocalGardenSubtitle => 'Clear intentions, habits, sessions, and notes.'; + String get deleteAllLocalDataSubtitle => 'Reset Focus Seed on this device.'; @override - String get dataImported => 'Data imported'; + String get todayDataCleared => 'Today\'s data cleared'; @override - String get importFailed => 'Import failed'; - - @override - String get localDataCleared => 'Local data cleared'; + String get localDataCleared => 'Local data deleted'; @override String get journeyTitle => 'Today\'s journey'; @@ -404,7 +409,8 @@ class AppLocalizationsEn extends AppLocalizations { String get sevenDayGrowth => '7-day growth'; @override - String get sevenDayGrowthSubtitle => 'Small actions become visible over time.'; + String get sevenDayGrowthSubtitle => + 'Small actions become visible over time.'; @override String get growthSeed => 'seed'; @@ -430,7 +436,8 @@ class AppLocalizationsEn extends AppLocalizations { String get growthSproutSemantic => 'A sprout growing from today\'s intention'; @override - String get growthLeavesSemantic => 'A young plant growing with today\'s progress'; + String get growthLeavesSemantic => + 'A young plant growing with today\'s progress'; @override String get growthFlowerSemantic => 'A flower marking a complete daily cycle'; diff --git a/lib/src/l10n/generated/app_localizations_zh.dart b/lib/src/l10n/generated/app_localizations_zh.dart index 20e2c45..4f82f55 100644 --- a/lib/src/l10n/generated/app_localizations_zh.dart +++ b/lib/src/l10n/generated/app_localizations_zh.dart @@ -218,9 +218,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get deleteHabitBody => '这个习惯的历史完成记录也会删除。'; - @override - String get exportJson => '导出 JSON'; - @override String get close => '关闭'; @@ -228,34 +225,32 @@ class AppLocalizationsZh extends AppLocalizations { String get copy => '复制'; @override - String get exportCopied => '已复制导出内容'; + String get clearTodayDataTitle => '清理今日数据?'; @override - String get importJson => '导入 JSON'; + String get clearTodayDataBody => '只清除今天的目标、打卡、专注记录和复盘,过去日期的数据会保留。'; @override - String get importHint => '粘贴 Focus Seed 的 JSON 导出内容'; + String get clearAllDataTitle => '删除全部本地数据?'; @override - String get import => '导入'; - - @override - String get clearDataTitle => '清除本地数据?'; - - @override - String get clearDataBody => '会清除本机的目标、习惯、专注记录和复盘。'; + String get clearAllDataBody => '会删除本机的目标、习惯、专注记录和复盘,无法撤销。'; @override String get clear => '清除'; + @override + String get deleteAll => '全部删除'; + @override String get privacyTitle => '隐私'; @override - String get privacyBodyOne => 'Focus Seed 只在本机保存目标、习惯、专注记录、复盘和设置。当前版本没有登录、云同步、广告、分析 SDK 或付费 SDK。'; + String get privacyBodyOne => + 'Focus Seed 只在本机保存目标、习惯、专注记录、复盘和设置。当前版本没有登录、云同步、广告、分析 SDK 或付费 SDK。'; @override - String get privacyBodyTwo => '你可以在“我的空间”导出、导入或清除本地数据。'; + String get privacyBodyTwo => '你可以随时清理今日进度,或删除本机的全部本地数据。'; @override String get reflectionEmptyError => '写一句真实的总结。'; @@ -324,31 +319,37 @@ class AppLocalizationsZh extends AppLocalizations { String get languageChinese => '中文'; @override - String get yourData => '数据'; + String get localData => '本地数据'; @override - String get exportJsonSubtitle => '复制一份本地备份。'; + String get focusDataLabel => '专注'; @override - String get importJsonSubtitle => '恢复之前导出的数据。'; + String get habitsDataLabel => '习惯'; + + @override + String get notesDataLabel => '记录'; + + @override + String get clearToday => '清理今日'; + + @override + String get clearTodaySubtitle => '只移除今天的进度。'; @override String get privacySubtitle => '数据只保存在本机。'; @override - String get resetLocalGarden => '清除本地数据'; + String get deleteAllLocalData => '删除全部本地数据'; @override - String get resetLocalGardenSubtitle => '清除目标、习惯、记录和复盘。'; + String get deleteAllLocalDataSubtitle => '重置这台设备上的 Focus Seed。'; @override - String get dataImported => '已导入数据'; + String get todayDataCleared => '今日数据已清理'; @override - String get importFailed => '导入失败'; - - @override - String get localDataCleared => '本地数据已清除'; + String get localDataCleared => '本地数据已删除'; @override String get journeyTitle => '今日进展'; diff --git a/lib/src/widgets/dialogs.dart b/lib/src/widgets/dialogs.dart index ccdc3db..b3c2613 100644 --- a/lib/src/widgets/dialogs.dart +++ b/lib/src/widgets/dialogs.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import '../core/json_utils.dart'; import '../l10n/l10n.dart'; -import '../models/app_state.dart'; Future showHabitDialog( BuildContext context, @@ -123,64 +120,16 @@ Future confirmDeleteHabit( ); } -Future showExportDialog(BuildContext context, AppState state) async { - final scaffoldContext = context; - final export = prettyJson(state.toJson()); - await showDialog( - context: context, - builder: - (context) => AlertDialog( - title: Text(context.l10n.exportJson), - content: SizedBox( - width: double.maxFinite, - child: SingleChildScrollView( - child: SelectableText( - export, - style: const TextStyle(fontFamily: 'monospace'), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.close), - ), - FilledButton.icon( - onPressed: () async { - final message = context.l10n.exportCopied; - await Clipboard.setData(ClipboardData(text: export)); - if (context.mounted) { - Navigator.pop(context); - showSnack(scaffoldContext, message); - } - }, - icon: const Icon(Icons.copy), - label: Text(context.l10n.copy), - ), - ], - ), - ); -} - -Future showImportDialog( +Future confirmClearToday( BuildContext context, - ValueChanged onImport, + VoidCallback onConfirm, ) async { - var value = ''; await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(context.l10n.importJson), - content: TextFormField( - minLines: 8, - maxLines: 12, - onChanged: (nextValue) => value = nextValue, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: context.l10n.importHint, - ), - ), + title: Text(context.l10n.clearTodayDataTitle), + content: Text(context.l10n.clearTodayDataBody), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -188,10 +137,10 @@ Future showImportDialog( ), FilledButton( onPressed: () { - onImport(value); + onConfirm(); Navigator.pop(context); }, - child: Text(context.l10n.import), + child: Text(context.l10n.clearToday), ), ], ), @@ -206,19 +155,23 @@ Future confirmClearData( context: context, builder: (context) => AlertDialog( - title: Text(context.l10n.clearDataTitle), - content: Text(context.l10n.clearDataBody), + title: Text(context.l10n.clearAllDataTitle), + content: Text(context.l10n.clearAllDataBody), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(context.l10n.cancel), ), FilledButton( + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), onPressed: () { onConfirm(); Navigator.pop(context); }, - child: Text(context.l10n.clear), + child: Text(context.l10n.deleteAll), ), ], ), diff --git a/test/focus_seed_controller_test.dart b/test/focus_seed_controller_test.dart new file mode 100644 index 0000000..cea2589 --- /dev/null +++ b/test/focus_seed_controller_test.dart @@ -0,0 +1,80 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:focus_seed/src/application/focus_seed_controller.dart'; +import 'package:focus_seed/src/core/date_keys.dart'; +import 'package:focus_seed/src/data/focus_seed_store.dart'; +import 'package:focus_seed/src/data/local/focus_seed_database.dart'; +import 'package:focus_seed/src/models/app_state.dart'; +import 'package:focus_seed/src/models/focus_session.dart'; +import 'package:focus_seed/src/models/habit.dart'; +import 'package:focus_seed/src/models/mood.dart'; +import 'package:focus_seed/src/models/reflection_entry.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('clearToday removes only current-day progress', () async { + final database = FocusSeedDatabase(NativeDatabase.memory()); + addTearDown(database.close); + final store = FocusSeedStore(database: database); + final today = todayKey(); + final yesterday = dateKey(DateTime.now().subtract(const Duration(days: 1))); + final habit = Habit( + id: 'habit-1', + title: 'Keep scope clear', + createdAt: DateTime.now().subtract(const Duration(days: 2)), + completedDates: {today, yesterday}, + ); + await store.save( + AppState( + anchor: 'Finish the cleanup', + anchorDateKey: today, + habits: [habit], + sessions: [ + FocusSession( + id: 'session-today', + minutes: 25, + completedAt: DateTime.now(), + ), + FocusSession( + id: 'session-yesterday', + minutes: 15, + completedAt: DateTime.now().subtract(const Duration(days: 1)), + ), + ], + reflections: [ + ReflectionEntry( + id: 'reflection-today', + dateKey: today, + note: 'Today note', + mood: Mood.clear, + createdAt: DateTime.now(), + ), + ReflectionEntry( + id: 'reflection-yesterday', + dateKey: yesterday, + note: 'Yesterday note', + mood: Mood.calm, + createdAt: DateTime.now().subtract(const Duration(days: 1)), + ), + ], + ), + ); + final controller = FocusSeedController(store: store); + await controller.load(); + + final cleared = await controller.clearToday(); + final state = controller.state!; + + expect(cleared, isTrue); + expect(state.activeAnchor(today), isEmpty); + expect(state.habits.single.completedDates, {yesterday}); + expect(state.sessions.map((session) => session.id), ['session-yesterday']); + expect(state.reflections.map((reflection) => reflection.id), [ + 'reflection-yesterday', + ]); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart index bfef17c..e42f109 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -49,6 +49,20 @@ void main() { expect(find.text('Appearance'), findsOneWidget); }); + testWidgets('Me manages local data without JSON tools', (tester) async { + await pumpFocusSeedApp(tester, locale: const Locale('en')); + + await tester.tap(find.text('Me')); + await tester.pumpAndSettle(); + + expect(find.text('Local data'), findsOneWidget); + expect(find.text('Clear today'), findsOneWidget); + await tester.scrollUntilVisible(find.text('Delete all local data'), 180); + expect(find.text('Delete all local data'), findsOneWidget); + expect(find.text('Export JSON'), findsNothing); + expect(find.text('Import JSON'), findsNothing); + }); + testWidgets('Today remains usable at narrow width and 200 percent text', ( tester, ) async {