feat: replace json tools with local data controls

This commit is contained in:
2026-06-12 01:11:50 +08:00
parent b88b947e66
commit b287a56731
10 changed files with 415 additions and 254 deletions
+31 -14
View File
@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -168,22 +166,41 @@ class FocusSeedController extends ChangeNotifier {
return _commit(current.copyWith(reflections: reflections)); return _commit(current.copyWith(reflections: reflections));
} }
Future<bool> importState(String raw) async {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map<String, dynamic>) {
return false;
}
return _commit(AppState.fromJson(decoded));
} on Object {
return false;
}
}
Future<bool> clearData() async { Future<bool> clearData() async {
return _commit(AppState.starter(defaultHabitTitles: _defaultHabitTitles)); return _commit(AppState.starter(defaultHabitTitles: _defaultHabitTitles));
} }
Future<bool> clearToday() async {
final current = _state;
if (current == null) {
return false;
}
final today = todayKey();
final habits = current.habits
.map(
(habit) => habit.copyWith(
completedDates: Set<String>.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<bool> _commit(AppState nextState) async { Future<bool> _commit(AppState nextState) async {
final previousState = _state; final previousState = _state;
_state = nextState; _state = nextState;
+107 -23
View File
@@ -113,35 +113,29 @@ class MeScreen extends StatelessWidget {
], ],
const SizedBox(height: 18), const SizedBox(height: 18),
Text( Text(
l10n.yourData, l10n.localData,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_DataSnapshotCard(
focusSessions: state.sessions.length,
habits: state.habits.length,
reflections: state.reflections.length,
),
const SizedBox(height: 12),
SurfacePanel( SurfacePanel(
padding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column( child: Column(
children: [ children: [
SettingsTile( SettingsTile(
icon: Icons.ios_share_outlined, icon: Icons.cleaning_services_outlined,
title: l10n.exportJson, title: l10n.clearToday,
subtitle: l10n.exportJsonSubtitle, subtitle: l10n.clearTodaySubtitle,
onTap: () => showExportDialog(context, state),
),
const Divider(),
SettingsTile(
icon: Icons.file_upload_outlined,
title: l10n.importJson,
subtitle: l10n.importJsonSubtitle,
onTap: onTap:
() => showImportDialog(context, (raw) async { () => confirmClearToday(context, () async {
final imported = await controller.importState(raw); final cleared = await controller.clearToday();
if (context.mounted) { if (context.mounted && cleared) {
showSnack( showSnack(context, l10n.todayDataCleared);
context,
imported
? l10n.dataImported
: l10n.importFailed,
);
} }
}), }),
), ),
@@ -159,9 +153,9 @@ class MeScreen extends StatelessWidget {
SurfacePanel( SurfacePanel(
padding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.symmetric(horizontal: 14),
child: SettingsTile( child: SettingsTile(
icon: Icons.delete_outline, icon: Icons.delete_forever_outlined,
title: l10n.resetLocalGarden, title: l10n.deleteAllLocalData,
subtitle: l10n.resetLocalGardenSubtitle, subtitle: l10n.deleteAllLocalDataSubtitle,
destructive: true, destructive: true,
onTap: onTap:
() => confirmClearData(context, () async { () => 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<double>(
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 { class _SectionHeader extends StatelessWidget {
const _SectionHeader({required this.title, required this.detail}); const _SectionHeader({required this.title, required this.detail});
+16 -16
View File
@@ -114,19 +114,17 @@
"intentionHint": "What matters most today?", "intentionHint": "What matters most today?",
"deleteHabitTitle": "Delete habit?", "deleteHabitTitle": "Delete habit?",
"deleteHabitBody": "Past completion records for this habit will be removed.", "deleteHabitBody": "Past completion records for this habit will be removed.",
"exportJson": "Export JSON",
"close": "Close", "close": "Close",
"copy": "Copy", "copy": "Copy",
"exportCopied": "Export copied", "clearTodayDataTitle": "Clear today?",
"importJson": "Import JSON", "clearTodayDataBody": "This removes today's intention, habit checks, focus sessions, and reflection. Past days stay untouched.",
"importHint": "Paste Focus Seed JSON export", "clearAllDataTitle": "Delete all local data?",
"import": "Import", "clearAllDataBody": "This deletes intentions, habits, focus sessions, and reflections on this device. It cannot be undone.",
"clearDataTitle": "Clear local data?",
"clearDataBody": "This resets habits, focus sessions, and reflections on this device.",
"clear": "Clear", "clear": "Clear",
"deleteAll": "Delete all",
"privacyTitle": "Privacy", "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.", "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.", "reflectionEmptyError": "Write one honest sentence.",
"reflectionSaveError": "Could not save. Please try again.", "reflectionSaveError": "Could not save. Please try again.",
"reflectionSheetTitle": "How did it go?", "reflectionSheetTitle": "How did it go?",
@@ -149,15 +147,17 @@
"languageDefault": "Default", "languageDefault": "Default",
"languageEnglish": "English", "languageEnglish": "English",
"languageChinese": "Chinese", "languageChinese": "Chinese",
"yourData": "Your data", "localData": "Local data",
"exportJsonSubtitle": "Copy a readable local backup.", "focusDataLabel": "Focus",
"importJsonSubtitle": "Restore a previous Focus Seed export.", "habitsDataLabel": "Habits",
"notesDataLabel": "Notes",
"clearToday": "Clear today",
"clearTodaySubtitle": "Remove only today's progress.",
"privacySubtitle": "Everything stays on this device.", "privacySubtitle": "Everything stays on this device.",
"resetLocalGarden": "Reset local garden", "deleteAllLocalData": "Delete all local data",
"resetLocalGardenSubtitle": "Clear intentions, habits, sessions, and notes.", "deleteAllLocalDataSubtitle": "Reset Focus Seed on this device.",
"dataImported": "Data imported", "todayDataCleared": "Today's data cleared",
"importFailed": "Import failed", "localDataCleared": "Local data deleted",
"localDataCleared": "Local data cleared",
"journeyTitle": "Today's journey", "journeyTitle": "Today's journey",
"recentFocus": "Recent focus", "recentFocus": "Recent focus",
"sessionsTotal": "{count} sessions total", "sessionsTotal": "{count} sessions total",
+16 -16
View File
@@ -114,19 +114,17 @@
"intentionHint": "\u4eca\u5929\u6700\u91cd\u8981\u7684\u662f\u4ec0\u4e48\uff1f", "intentionHint": "\u4eca\u5929\u6700\u91cd\u8981\u7684\u662f\u4ec0\u4e48\uff1f",
"deleteHabitTitle": "\u5220\u9664\u4e60\u60ef\uff1f", "deleteHabitTitle": "\u5220\u9664\u4e60\u60ef\uff1f",
"deleteHabitBody": "\u8fd9\u4e2a\u4e60\u60ef\u7684\u5386\u53f2\u5b8c\u6210\u8bb0\u5f55\u4e5f\u4f1a\u5220\u9664\u3002", "deleteHabitBody": "\u8fd9\u4e2a\u4e60\u60ef\u7684\u5386\u53f2\u5b8c\u6210\u8bb0\u5f55\u4e5f\u4f1a\u5220\u9664\u3002",
"exportJson": "\u5bfc\u51fa JSON",
"close": "\u5173\u95ed", "close": "\u5173\u95ed",
"copy": "\u590d\u5236", "copy": "\u590d\u5236",
"exportCopied": "\u5df2\u590d\u5236\u5bfc\u51fa\u5185\u5bb9", "clearTodayDataTitle": "清理今日数据?",
"importJson": "\u5bfc\u5165 JSON", "clearTodayDataBody": "只清除今天的目标、打卡、专注记录和复盘,过去日期的数据会保留。",
"importHint": "\u7c98\u8d34 Focus Seed \u7684 JSON \u5bfc\u51fa\u5185\u5bb9", "clearAllDataTitle": "删除全部本地数据?",
"import": "\u5bfc\u5165", "clearAllDataBody": "会删除本机的目标、习惯、专注记录和复盘,无法撤销。",
"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",
"clear": "\u6e05\u9664", "clear": "\u6e05\u9664",
"deleteAll": "全部删除",
"privacyTitle": "\u9690\u79c1", "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", "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", "reflectionEmptyError": "\u5199\u4e00\u53e5\u771f\u5b9e\u7684\u603b\u7ed3\u3002",
"reflectionSaveError": "\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002", "reflectionSaveError": "\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002",
"reflectionSheetTitle": "\u590d\u76d8\u4e00\u4e0b", "reflectionSheetTitle": "\u590d\u76d8\u4e00\u4e0b",
@@ -149,15 +147,17 @@
"languageDefault": "\u9ed8\u8ba4", "languageDefault": "\u9ed8\u8ba4",
"languageEnglish": "English", "languageEnglish": "English",
"languageChinese": "\u4e2d\u6587", "languageChinese": "\u4e2d\u6587",
"yourData": "\u6570\u636e", "localData": "本地数据",
"exportJsonSubtitle": "\u590d\u5236\u4e00\u4efd\u672c\u5730\u5907\u4efd\u3002", "focusDataLabel": "专注",
"importJsonSubtitle": "\u6062\u590d\u4e4b\u524d\u5bfc\u51fa\u7684\u6570\u636e\u3002", "habitsDataLabel": "习惯",
"notesDataLabel": "记录",
"clearToday": "清理今日",
"clearTodaySubtitle": "只移除今天的进度。",
"privacySubtitle": "\u6570\u636e\u53ea\u4fdd\u5b58\u5728\u672c\u673a\u3002", "privacySubtitle": "\u6570\u636e\u53ea\u4fdd\u5b58\u5728\u672c\u673a\u3002",
"resetLocalGarden": "\u6e05\u9664\u672c\u5730\u6570\u636e", "deleteAllLocalData": "删除全部本地数据",
"resetLocalGardenSubtitle": "\u6e05\u9664\u76ee\u6807\u3001\u4e60\u60ef\u3001\u8bb0\u5f55\u548c\u590d\u76d8\u3002", "deleteAllLocalDataSubtitle": "重置这台设备上的 Focus Seed。",
"dataImported": "\u5df2\u5bfc\u5165\u6570\u636e", "todayDataCleared": "今日数据已清理",
"importFailed": "\u5bfc\u5165\u5931\u8d25", "localDataCleared": "本地数据已删除",
"localDataCleared": "\u672c\u5730\u6570\u636e\u5df2\u6e05\u9664",
"journeyTitle": "\u4eca\u65e5\u8fdb\u5c55", "journeyTitle": "\u4eca\u65e5\u8fdb\u5c55",
"recentFocus": "\u6700\u8fd1\u4e13\u6ce8", "recentFocus": "\u6700\u8fd1\u4e13\u6ce8",
"sessionsTotal": "\u5171 {count} \u6b21", "sessionsTotal": "\u5171 {count} \u6b21",
+72 -67
View File
@@ -62,7 +62,8 @@ import 'app_localizations_zh.dart';
/// be consistent with the languages listed in the AppLocalizations.supportedLocales /// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property. /// property.
abstract class AppLocalizations { abstract class AppLocalizations {
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName; final String localeName;
@@ -70,7 +71,8 @@ abstract class AppLocalizations {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!; return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
} }
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate(); static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations /// A list of this localizations delegate along with the default localizations
/// delegates. /// delegates.
@@ -82,7 +84,8 @@ abstract class AppLocalizations {
/// Additional delegates can be added by appending to this list in /// 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 /// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required. /// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[ static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate, delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
@@ -92,7 +95,7 @@ abstract class AppLocalizations {
/// A list of this localizations delegate's supported locales. /// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[ static const List<Locale> supportedLocales = <Locale>[
Locale('en'), Locale('en'),
Locale('zh') Locale('zh'),
]; ];
/// No description provided for @appTitle. /// No description provided for @appTitle.
@@ -491,12 +494,6 @@ abstract class AppLocalizations {
/// **'Past completion records for this habit will be removed.'** /// **'Past completion records for this habit will be removed.'**
String get deleteHabitBody; String get deleteHabitBody;
/// No description provided for @exportJson.
///
/// In en, this message translates to:
/// **'Export JSON'**
String get exportJson;
/// No description provided for @close. /// No description provided for @close.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -509,41 +506,29 @@ abstract class AppLocalizations {
/// **'Copy'** /// **'Copy'**
String get copy; String get copy;
/// No description provided for @exportCopied. /// No description provided for @clearTodayDataTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Export copied'** /// **'Clear today?'**
String get exportCopied; String get clearTodayDataTitle;
/// No description provided for @importJson. /// No description provided for @clearTodayDataBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Import JSON'** /// **'This removes today\'s intention, habit checks, focus sessions, and reflection. Past days stay untouched.'**
String get importJson; String get clearTodayDataBody;
/// No description provided for @importHint. /// No description provided for @clearAllDataTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Paste Focus Seed JSON export'** /// **'Delete all local data?'**
String get importHint; String get clearAllDataTitle;
/// No description provided for @import. /// No description provided for @clearAllDataBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Import'** /// **'This deletes intentions, habits, focus sessions, and reflections on this device. It cannot be undone.'**
String get import; String get clearAllDataBody;
/// 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;
/// No description provided for @clear. /// No description provided for @clear.
/// ///
@@ -551,6 +536,12 @@ abstract class AppLocalizations {
/// **'Clear'** /// **'Clear'**
String get 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. /// No description provided for @privacyTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -566,7 +557,7 @@ abstract class AppLocalizations {
/// No description provided for @privacyBodyTwo. /// No description provided for @privacyBodyTwo.
/// ///
/// In en, this message translates to: /// 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; String get privacyBodyTwo;
/// No description provided for @reflectionEmptyError. /// No description provided for @reflectionEmptyError.
@@ -701,23 +692,41 @@ abstract class AppLocalizations {
/// **'Chinese'** /// **'Chinese'**
String get languageChinese; String get languageChinese;
/// No description provided for @yourData. /// No description provided for @localData.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Your data'** /// **'Local data'**
String get yourData; String get localData;
/// No description provided for @exportJsonSubtitle. /// No description provided for @focusDataLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Copy a readable local backup.'** /// **'Focus'**
String get exportJsonSubtitle; String get focusDataLabel;
/// No description provided for @importJsonSubtitle. /// No description provided for @habitsDataLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Restore a previous Focus Seed export.'** /// **'Habits'**
String get importJsonSubtitle; 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. /// No description provided for @privacySubtitle.
/// ///
@@ -725,34 +734,28 @@ abstract class AppLocalizations {
/// **'Everything stays on this device.'** /// **'Everything stays on this device.'**
String get privacySubtitle; String get privacySubtitle;
/// No description provided for @resetLocalGarden. /// No description provided for @deleteAllLocalData.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Reset local garden'** /// **'Delete all local data'**
String get resetLocalGarden; String get deleteAllLocalData;
/// No description provided for @resetLocalGardenSubtitle. /// No description provided for @deleteAllLocalDataSubtitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Clear intentions, habits, sessions, and notes.'** /// **'Reset Focus Seed on this device.'**
String get resetLocalGardenSubtitle; String get deleteAllLocalDataSubtitle;
/// No description provided for @dataImported. /// No description provided for @todayDataCleared.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Data imported'** /// **'Today\'s data cleared'**
String get dataImported; String get todayDataCleared;
/// No description provided for @importFailed.
///
/// In en, this message translates to:
/// **'Import failed'**
String get importFailed;
/// No description provided for @localDataCleared. /// No description provided for @localDataCleared.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Local data cleared'** /// **'Local data deleted'**
String get localDataCleared; String get localDataCleared;
/// No description provided for @journeyTitle. /// No description provided for @journeyTitle.
@@ -924,7 +927,8 @@ abstract class AppLocalizations {
String get defaultHabitReflect; String get defaultHabitReflect;
} }
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate(); const _AppLocalizationsDelegate();
@override @override
@@ -933,25 +937,26 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations>
} }
@override @override
bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode); bool isSupported(Locale locale) =>
<String>['en', 'zh'].contains(locale.languageCode);
@override @override
bool shouldReload(_AppLocalizationsDelegate old) => false; bool shouldReload(_AppLocalizationsDelegate old) => false;
} }
AppLocalizations lookupAppLocalizations(Locale locale) { AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified. // Lookup logic when only language code is specified.
switch (locale.languageCode) { switch (locale.languageCode) {
case 'en': return AppLocalizationsEn(); case 'en':
case 'zh': return AppLocalizationsZh(); return AppLocalizationsEn();
case 'zh':
return AppLocalizationsZh();
} }
throw FlutterError( throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue ' 'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration ' 'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.' 'that was used.',
); );
} }
@@ -216,10 +216,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get deleteHabitTitle => 'Delete habit?'; String get deleteHabitTitle => 'Delete habit?';
@override @override
String get deleteHabitBody => 'Past completion records for this habit will be removed.'; String get deleteHabitBody =>
'Past completion records for this habit will be removed.';
@override
String get exportJson => 'Export JSON';
@override @override
String get close => 'Close'; String get close => 'Close';
@@ -228,34 +226,35 @@ class AppLocalizationsEn extends AppLocalizations {
String get copy => 'Copy'; String get copy => 'Copy';
@override @override
String get exportCopied => 'Export copied'; String get clearTodayDataTitle => 'Clear today?';
@override @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 @override
String get importHint => 'Paste Focus Seed JSON export'; String get clearAllDataTitle => 'Delete all local data?';
@override @override
String get import => 'Import'; String get clearAllDataBody =>
'This deletes intentions, habits, focus sessions, and reflections on this device. It cannot be undone.';
@override
String get clearDataTitle => 'Clear local data?';
@override
String get clearDataBody => 'This resets habits, focus sessions, and reflections on this device.';
@override @override
String get clear => 'Clear'; String get clear => 'Clear';
@override
String get deleteAll => 'Delete all';
@override @override
String get privacyTitle => 'Privacy'; String get privacyTitle => 'Privacy';
@override @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 @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 @override
String get reflectionEmptyError => 'Write one honest sentence.'; String get reflectionEmptyError => 'Write one honest sentence.';
@@ -324,31 +323,37 @@ class AppLocalizationsEn extends AppLocalizations {
String get languageChinese => 'Chinese'; String get languageChinese => 'Chinese';
@override @override
String get yourData => 'Your data'; String get localData => 'Local data';
@override @override
String get exportJsonSubtitle => 'Copy a readable local backup.'; String get focusDataLabel => 'Focus';
@override @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 @override
String get privacySubtitle => 'Everything stays on this device.'; String get privacySubtitle => 'Everything stays on this device.';
@override @override
String get resetLocalGarden => 'Reset local garden'; String get deleteAllLocalData => 'Delete all local data';
@override @override
String get resetLocalGardenSubtitle => 'Clear intentions, habits, sessions, and notes.'; String get deleteAllLocalDataSubtitle => 'Reset Focus Seed on this device.';
@override @override
String get dataImported => 'Data imported'; String get todayDataCleared => 'Today\'s data cleared';
@override @override
String get importFailed => 'Import failed'; String get localDataCleared => 'Local data deleted';
@override
String get localDataCleared => 'Local data cleared';
@override @override
String get journeyTitle => 'Today\'s journey'; String get journeyTitle => 'Today\'s journey';
@@ -404,7 +409,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get sevenDayGrowth => '7-day growth'; String get sevenDayGrowth => '7-day growth';
@override @override
String get sevenDayGrowthSubtitle => 'Small actions become visible over time.'; String get sevenDayGrowthSubtitle =>
'Small actions become visible over time.';
@override @override
String get growthSeed => 'seed'; String get growthSeed => 'seed';
@@ -430,7 +436,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get growthSproutSemantic => 'A sprout growing from today\'s intention'; String get growthSproutSemantic => 'A sprout growing from today\'s intention';
@override @override
String get growthLeavesSemantic => 'A young plant growing with today\'s progress'; String get growthLeavesSemantic =>
'A young plant growing with today\'s progress';
@override @override
String get growthFlowerSemantic => 'A flower marking a complete daily cycle'; String get growthFlowerSemantic => 'A flower marking a complete daily cycle';
@@ -218,9 +218,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get deleteHabitBody => '这个习惯的历史完成记录也会删除。'; String get deleteHabitBody => '这个习惯的历史完成记录也会删除。';
@override
String get exportJson => '导出 JSON';
@override @override
String get close => '关闭'; String get close => '关闭';
@@ -228,34 +225,32 @@ class AppLocalizationsZh extends AppLocalizations {
String get copy => '复制'; String get copy => '复制';
@override @override
String get exportCopied => '已复制导出内容'; String get clearTodayDataTitle => '清理今日数据?';
@override @override
String get importJson => '导入 JSON'; String get clearTodayDataBody => '只清除今天的目标、打卡、专注记录和复盘,过去日期的数据会保留。';
@override @override
String get importHint => '粘贴 Focus Seed 的 JSON 导出内容'; String get clearAllDataTitle => '删除全部本地数据?';
@override @override
String get import => '导入'; String get clearAllDataBody => '会删除本机的目标、习惯、专注记录和复盘,无法撤销。';
@override
String get clearDataTitle => '清除本地数据?';
@override
String get clearDataBody => '会清除本机的目标、习惯、专注记录和复盘。';
@override @override
String get clear => '清除'; String get clear => '清除';
@override
String get deleteAll => '全部删除';
@override @override
String get privacyTitle => '隐私'; String get privacyTitle => '隐私';
@override @override
String get privacyBodyOne => 'Focus Seed 只在本机保存目标、习惯、专注记录、复盘和设置。当前版本没有登录、云同步、广告、分析 SDK 或付费 SDK。'; String get privacyBodyOne =>
'Focus Seed 只在本机保存目标、习惯、专注记录、复盘和设置。当前版本没有登录、云同步、广告、分析 SDK 或付费 SDK。';
@override @override
String get privacyBodyTwo => '你可以在“我的空间”导出、导入或清除本地数据。'; String get privacyBodyTwo => '你可以随时清理今日进度,或删除本机的全部本地数据。';
@override @override
String get reflectionEmptyError => '写一句真实的总结。'; String get reflectionEmptyError => '写一句真实的总结。';
@@ -324,31 +319,37 @@ class AppLocalizationsZh extends AppLocalizations {
String get languageChinese => '中文'; String get languageChinese => '中文';
@override @override
String get yourData => '数据'; String get localData => '本地数据';
@override @override
String get exportJsonSubtitle => '复制一份本地备份。'; String get focusDataLabel => '专注';
@override @override
String get importJsonSubtitle => '恢复之前导出的数据。'; String get habitsDataLabel => '习惯';
@override
String get notesDataLabel => '记录';
@override
String get clearToday => '清理今日';
@override
String get clearTodaySubtitle => '只移除今天的进度。';
@override @override
String get privacySubtitle => '数据只保存在本机。'; String get privacySubtitle => '数据只保存在本机。';
@override @override
String get resetLocalGarden => '清除本地数据'; String get deleteAllLocalData => '删除全部本地数据';
@override @override
String get resetLocalGardenSubtitle => '清除目标、习惯、记录和复盘。'; String get deleteAllLocalDataSubtitle => '重置这台设备上的 Focus Seed。';
@override @override
String get dataImported => '已导入数据'; String get todayDataCleared => '今日数据已清理';
@override @override
String get importFailed => '导入失败'; String get localDataCleared => '本地数据已删除';
@override
String get localDataCleared => '本地数据已清除';
@override @override
String get journeyTitle => '今日进展'; String get journeyTitle => '今日进展';
+13 -60
View File
@@ -1,9 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../core/json_utils.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/app_state.dart';
Future<void> showHabitDialog( Future<void> showHabitDialog(
BuildContext context, BuildContext context,
@@ -123,64 +120,16 @@ Future<void> confirmDeleteHabit(
); );
} }
Future<void> showExportDialog(BuildContext context, AppState state) async { Future<void> confirmClearToday(
final scaffoldContext = context;
final export = prettyJson(state.toJson());
await showDialog<void>(
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<void> showImportDialog(
BuildContext context, BuildContext context,
ValueChanged<String> onImport, VoidCallback onConfirm,
) async { ) async {
var value = '';
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: builder:
(context) => AlertDialog( (context) => AlertDialog(
title: Text(context.l10n.importJson), title: Text(context.l10n.clearTodayDataTitle),
content: TextFormField( content: Text(context.l10n.clearTodayDataBody),
minLines: 8,
maxLines: 12,
onChanged: (nextValue) => value = nextValue,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: context.l10n.importHint,
),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
@@ -188,10 +137,10 @@ Future<void> showImportDialog(
), ),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
onImport(value); onConfirm();
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text(context.l10n.import), child: Text(context.l10n.clearToday),
), ),
], ],
), ),
@@ -206,19 +155,23 @@ Future<void> confirmClearData(
context: context, context: context,
builder: builder:
(context) => AlertDialog( (context) => AlertDialog(
title: Text(context.l10n.clearDataTitle), title: Text(context.l10n.clearAllDataTitle),
content: Text(context.l10n.clearDataBody), content: Text(context.l10n.clearAllDataBody),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text(context.l10n.cancel), child: Text(context.l10n.cancel),
), ),
FilledButton( FilledButton(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
onPressed: () { onPressed: () {
onConfirm(); onConfirm();
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text(context.l10n.clear), child: Text(context.l10n.deleteAll),
), ),
], ],
), ),
+80
View File
@@ -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',
]);
});
}
+14
View File
@@ -49,6 +49,20 @@ void main() {
expect(find.text('Appearance'), findsOneWidget); 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', ( testWidgets('Today remains usable at narrow width and 200 percent text', (
tester, tester,
) async { ) async {