feat: replace json tools with local data controls
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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,17 +84,18 @@ 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 =
|
||||||
delegate,
|
<LocalizationsDelegate<dynamic>>[
|
||||||
GlobalMaterialLocalizations.delegate,
|
delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
];
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
];
|
||||||
|
|
||||||
/// 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 => '今日进展';
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user