首次远程推送
This commit is contained in:
+87
-110
@@ -4,33 +4,38 @@ import '../core/ambience.dart';
|
||||
|
||||
class AppTheme {
|
||||
static const ink = Color(0xFF17211C);
|
||||
static const leaf = Color(0xFF4D8B63);
|
||||
static const mint = Color(0xFFA9E8B5);
|
||||
static const ultramarine = Color(0xFF4E63D8);
|
||||
static const coral = Color(0xFFEA725E);
|
||||
static const violet = Color(0xFF7856D8);
|
||||
static const porcelain = Color(0xFFF7F8F5);
|
||||
static const forest = Color(0xFF08785B);
|
||||
static const teal = Color(0xFF0A8179);
|
||||
static const moss = Color(0xFF6C8F6B);
|
||||
static const mint = Color(0xFFA8E6C1);
|
||||
static const ultramarine = Color(0xFF2E6FDB);
|
||||
static const sky = Color(0xFF42A5F5);
|
||||
static const amber = Color(0xFFE3A321);
|
||||
static const coral = Color(0xFFD9654F);
|
||||
static const violet = Color(0xFF7257C8);
|
||||
static const paper = Color(0xFFFAFBFF);
|
||||
static const porcelain = Color(0xFFF4F7FC);
|
||||
|
||||
static ThemeData light(DayPhase phase) {
|
||||
final surface = switch (phase) {
|
||||
DayPhase.dawn => const Color(0xFFF3F8F7),
|
||||
DayPhase.dawn => const Color(0xFFF3F8FC),
|
||||
DayPhase.day => porcelain,
|
||||
DayPhase.dusk => const Color(0xFFFBF5F2),
|
||||
DayPhase.night => const Color(0xFFF3F5F3),
|
||||
DayPhase.dusk => const Color(0xFFFBF6F4),
|
||||
DayPhase.night => const Color(0xFFF2F5FA),
|
||||
};
|
||||
final scheme = ColorScheme.fromSeed(seedColor: leaf).copyWith(
|
||||
primary: leaf,
|
||||
final scheme = ColorScheme.fromSeed(seedColor: ultramarine).copyWith(
|
||||
primary: ultramarine,
|
||||
onPrimary: Colors.white,
|
||||
secondary: ultramarine,
|
||||
tertiary: coral,
|
||||
tertiary: amber,
|
||||
surface: surface,
|
||||
onSurface: ink,
|
||||
surfaceContainerLowest: Colors.white.withValues(alpha: 0.82),
|
||||
surfaceContainerLow: Colors.white.withValues(alpha: 0.72),
|
||||
surfaceContainer: const Color(0xFFF0F3EF),
|
||||
surfaceContainerHigh: const Color(0xFFE9EEE9),
|
||||
outline: const Color(0xFFB9C2BA),
|
||||
outlineVariant: const Color(0xFFDDE3DD),
|
||||
surfaceContainerLowest: const Color(0xFFFFFEFA),
|
||||
surfaceContainerLow: const Color(0xFFEFF3FA),
|
||||
surfaceContainer: const Color(0xFFE4EAF4),
|
||||
surfaceContainerHigh: const Color(0xFFD6DEEB),
|
||||
outline: const Color(0xFF9EA9B8),
|
||||
outlineVariant: const Color(0xFFD5DEEA),
|
||||
);
|
||||
return _base(scheme, phase);
|
||||
}
|
||||
@@ -42,50 +47,32 @@ class AppTheme {
|
||||
DayPhase.dusk => const Color(0xFF1A181A),
|
||||
DayPhase.night => const Color(0xFF101614),
|
||||
};
|
||||
final scheme = ColorScheme.fromSeed(
|
||||
seedColor: mint,
|
||||
brightness: Brightness.dark,
|
||||
).copyWith(
|
||||
primary: mint,
|
||||
onPrimary: ink,
|
||||
secondary: const Color(0xFFB9C2FF),
|
||||
tertiary: const Color(0xFFFFB4A8),
|
||||
final scheme = ColorScheme.fromSeed(seedColor: mint, brightness: Brightness.dark).copyWith(
|
||||
primary: const Color(0xFF8FBAFF),
|
||||
onPrimary: const Color(0xFF071226),
|
||||
secondary: const Color(0xFF8FBAFF),
|
||||
tertiary: const Color(0xFFFFD782),
|
||||
surface: surface,
|
||||
onSurface: const Color(0xFFF1F5F1),
|
||||
surfaceContainerLowest: const Color(0xFF0D1210),
|
||||
surfaceContainerLow: const Color(0xFF171E1B),
|
||||
surfaceContainer: const Color(0xFF1C2420),
|
||||
surfaceContainerHigh: const Color(0xFF243029),
|
||||
outline: const Color(0xFF7E8D83),
|
||||
outlineVariant: const Color(0xFF344039),
|
||||
surfaceContainerLow: const Color(0xFF151C24),
|
||||
surfaceContainer: const Color(0xFF1B2430),
|
||||
surfaceContainerHigh: const Color(0xFF253142),
|
||||
outline: const Color(0xFF7F8EA4),
|
||||
outlineVariant: const Color(0xFF344153),
|
||||
);
|
||||
return _base(scheme, phase);
|
||||
}
|
||||
|
||||
static ThemeData _base(ColorScheme scheme, DayPhase phase) {
|
||||
final base = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
);
|
||||
final base = ThemeData(useMaterial3: true, colorScheme: scheme, scaffoldBackgroundColor: scheme.surface);
|
||||
final textTheme = base.textTheme.copyWith(
|
||||
headlineSmall: base.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.15,
|
||||
),
|
||||
titleLarge: base.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.2,
|
||||
),
|
||||
titleMedium: base.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
),
|
||||
headlineSmall: base.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700, height: 1.15),
|
||||
titleLarge: base.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700, height: 1.2),
|
||||
titleMedium: base.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, height: 1.25),
|
||||
bodyLarge: base.textTheme.bodyLarge?.copyWith(height: 1.4),
|
||||
bodyMedium: base.textTheme.bodyMedium?.copyWith(height: 1.4),
|
||||
labelLarge: base.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
labelLarge: base.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
|
||||
);
|
||||
return base.copyWith(
|
||||
textTheme: textTheme,
|
||||
@@ -94,34 +81,22 @@ class AppTheme {
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
color: scheme.surfaceContainerLowest,
|
||||
shadowColor: ink.withValues(alpha: 0.12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
shadowColor: ink.withValues(alpha: 0.10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
height: 68,
|
||||
height: 72,
|
||||
elevation: 0,
|
||||
backgroundColor: scheme.surfaceContainerLowest,
|
||||
indicatorColor: scheme.primary.withValues(alpha: 0.14),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith(
|
||||
(states) => TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
states.contains(WidgetState.selected)
|
||||
? FontWeight.w700
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
indicatorColor: scheme.primary.withValues(alpha: 0.13),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) => IconThemeData(size: 23, color: states.contains(WidgetState.selected) ? scheme.primary : scheme.onSurfaceVariant)),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) => TextStyle(fontSize: 12, fontWeight: states.contains(WidgetState.selected) ? FontWeight.w700 : FontWeight.w500)),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(48, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
@@ -129,47 +104,24 @@ class AppTheme {
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(48, 48),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: scheme.surfaceContainerLow,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 13,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
),
|
||||
chipTheme: base.chipTheme.copyWith(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
side: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: scheme.outlineVariant,
|
||||
space: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
backgroundColor: scheme.surfaceContainerLowest,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: scheme.outlineVariant.withValues(alpha: 0.22))),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: scheme.outlineVariant.withValues(alpha: 0.22))),
|
||||
),
|
||||
chipTheme: base.chipTheme.copyWith(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), side: BorderSide(color: scheme.outlineVariant.withValues(alpha: 0.38))),
|
||||
dividerTheme: DividerThemeData(color: scheme.outlineVariant.withValues(alpha: 0.64), space: 1, thickness: 1),
|
||||
dialogTheme: DialogTheme(backgroundColor: scheme.surfaceContainerLowest, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: scheme.surface,
|
||||
modalBackgroundColor: scheme.surface,
|
||||
showDragHandle: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
),
|
||||
extensions: [FocusSeedVisuals.forPhase(phase, scheme.brightness)],
|
||||
);
|
||||
@@ -180,14 +132,26 @@ class AppTheme {
|
||||
class FocusSeedVisuals extends ThemeExtension<FocusSeedVisuals> {
|
||||
const FocusSeedVisuals({
|
||||
required this.ambientTint,
|
||||
required this.brand,
|
||||
required this.action,
|
||||
required this.focus,
|
||||
required this.habit,
|
||||
required this.reflection,
|
||||
required this.success,
|
||||
required this.privacy,
|
||||
required this.warning,
|
||||
required this.shadow,
|
||||
});
|
||||
|
||||
final Color ambientTint;
|
||||
final Color brand;
|
||||
final Color action;
|
||||
final Color focus;
|
||||
final Color habit;
|
||||
final Color reflection;
|
||||
final Color success;
|
||||
final Color privacy;
|
||||
final Color warning;
|
||||
final Color shadow;
|
||||
|
||||
static FocusSeedVisuals of(BuildContext context) {
|
||||
@@ -203,24 +167,31 @@ class FocusSeedVisuals extends ThemeExtension<FocusSeedVisuals> {
|
||||
DayPhase.night => const Color(0xFF9A89D8),
|
||||
};
|
||||
return FocusSeedVisuals(
|
||||
ambientTint: ambientTint.withValues(alpha: isDark ? 0.18 : 0.22),
|
||||
reflection: isDark ? const Color(0xFFBDAAFF) : AppTheme.violet,
|
||||
success: isDark ? AppTheme.mint : AppTheme.leaf,
|
||||
ambientTint: ambientTint.withValues(alpha: isDark ? 0.14 : 0.18),
|
||||
brand: isDark ? const Color(0xFFF1F5F1) : AppTheme.ink,
|
||||
action: isDark ? const Color(0xFFFFB09D) : AppTheme.coral,
|
||||
focus: isDark ? const Color(0xFF8FBAFF) : AppTheme.ultramarine,
|
||||
habit: isDark ? const Color(0xFFFFD782) : AppTheme.amber,
|
||||
reflection: isDark ? const Color(0xFFC7B8FF) : AppTheme.violet,
|
||||
success: isDark ? AppTheme.mint : AppTheme.forest,
|
||||
privacy: isDark ? const Color(0xFF8DDFC1) : AppTheme.forest,
|
||||
warning: isDark ? const Color(0xFFFFB4A8) : AppTheme.coral,
|
||||
shadow: Colors.black.withValues(alpha: isDark ? 0.32 : 0.10),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FocusSeedVisuals copyWith({
|
||||
Color? ambientTint,
|
||||
Color? reflection,
|
||||
Color? success,
|
||||
Color? shadow,
|
||||
}) {
|
||||
FocusSeedVisuals copyWith({Color? ambientTint, Color? brand, Color? action, Color? focus, Color? habit, Color? reflection, Color? success, Color? privacy, Color? warning, Color? shadow}) {
|
||||
return FocusSeedVisuals(
|
||||
ambientTint: ambientTint ?? this.ambientTint,
|
||||
brand: brand ?? this.brand,
|
||||
action: action ?? this.action,
|
||||
focus: focus ?? this.focus,
|
||||
habit: habit ?? this.habit,
|
||||
reflection: reflection ?? this.reflection,
|
||||
success: success ?? this.success,
|
||||
privacy: privacy ?? this.privacy,
|
||||
warning: warning ?? this.warning,
|
||||
shadow: shadow ?? this.shadow,
|
||||
);
|
||||
}
|
||||
@@ -232,8 +203,14 @@ class FocusSeedVisuals extends ThemeExtension<FocusSeedVisuals> {
|
||||
}
|
||||
return FocusSeedVisuals(
|
||||
ambientTint: Color.lerp(ambientTint, other.ambientTint, t)!,
|
||||
brand: Color.lerp(brand, other.brand, t)!,
|
||||
action: Color.lerp(action, other.action, t)!,
|
||||
focus: Color.lerp(focus, other.focus, t)!,
|
||||
habit: Color.lerp(habit, other.habit, t)!,
|
||||
reflection: Color.lerp(reflection, other.reflection, t)!,
|
||||
success: Color.lerp(success, other.success, t)!,
|
||||
privacy: Color.lerp(privacy, other.privacy, t)!,
|
||||
warning: Color.lerp(warning, other.warning, t)!,
|
||||
shadow: Color.lerp(shadow, other.shadow, t)!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import '../core/ambience.dart';
|
||||
import '../data/app_preferences_store.dart';
|
||||
import '../data/focus_seed_store.dart';
|
||||
import '../features/onboarding/welcome_screen.dart';
|
||||
import '../features/shell/focus_seed_home.dart';
|
||||
import '../l10n/generated/app_localizations.dart';
|
||||
import '../models/appearance_preference.dart';
|
||||
import '../models/language_preference.dart';
|
||||
import '../widgets/loading_view.dart';
|
||||
import 'app_theme.dart';
|
||||
|
||||
@visibleForTesting
|
||||
@@ -19,10 +21,16 @@ Locale focusSeedDefaultLocale({bool isRelease = kReleaseMode}) {
|
||||
}
|
||||
|
||||
class FocusSeedApp extends StatefulWidget {
|
||||
const FocusSeedApp({this.locale, this.focusSeedStore, super.key});
|
||||
const FocusSeedApp({
|
||||
this.locale,
|
||||
this.focusSeedStore,
|
||||
this.showWelcomeOnFirstRun = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Locale? locale;
|
||||
final FocusSeedStore? focusSeedStore;
|
||||
final bool showWelcomeOnFirstRun;
|
||||
|
||||
@override
|
||||
State<FocusSeedApp> createState() => _FocusSeedAppState();
|
||||
@@ -33,6 +41,10 @@ class _FocusSeedAppState extends State<FocusSeedApp>
|
||||
final _preferencesStore = AppPreferencesStore();
|
||||
AppearancePreference _appearance = AppearancePreference.system;
|
||||
LanguagePreference _language = LanguagePreference.defaultLocale;
|
||||
bool _onboardingComplete = true;
|
||||
bool _landingSetupComplete = true;
|
||||
bool _showLandingSetup = false;
|
||||
bool _preferencesLoaded = false;
|
||||
DayPhase _dayPhase = dayPhaseFor(DateTime.now());
|
||||
Timer? _phaseTimer;
|
||||
|
||||
@@ -52,14 +64,25 @@ class _FocusSeedAppState extends State<FocusSeedApp>
|
||||
}
|
||||
|
||||
Future<void> _loadPreferences() async {
|
||||
final appearance = await _preferencesStore.loadAppearance();
|
||||
final language = await _preferencesStore.loadLanguage();
|
||||
final (appearance, language, firstRunProgress) =
|
||||
await (
|
||||
_preferencesStore.loadAppearance(),
|
||||
_preferencesStore.loadLanguage(),
|
||||
_preferencesStore.loadFirstRunProgress(),
|
||||
).wait;
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_appearance = appearance;
|
||||
_language = language;
|
||||
_onboardingComplete = firstRunProgress.onboardingComplete;
|
||||
_landingSetupComplete = firstRunProgress.landingSetupComplete;
|
||||
_showLandingSetup =
|
||||
widget.showWelcomeOnFirstRun &&
|
||||
firstRunProgress.onboardingComplete &&
|
||||
!firstRunProgress.landingSetupComplete;
|
||||
_preferencesLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,6 +121,22 @@ class _FocusSeedAppState extends State<FocusSeedApp>
|
||||
return widget.locale ?? _language.locale ?? focusSeedDefaultLocale();
|
||||
}
|
||||
|
||||
Future<void> _completeOnboarding() async {
|
||||
setState(() {
|
||||
_onboardingComplete = true;
|
||||
_showLandingSetup = true;
|
||||
});
|
||||
await _preferencesStore.saveOnboardingComplete();
|
||||
}
|
||||
|
||||
Future<void> _completeLandingSetup() async {
|
||||
setState(() {
|
||||
_landingSetupComplete = true;
|
||||
_showLandingSetup = false;
|
||||
});
|
||||
await _preferencesStore.saveLandingSetupComplete();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
@@ -115,14 +154,28 @@ class _FocusSeedAppState extends State<FocusSeedApp>
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: FocusSeedHome(
|
||||
focusSeedStore: widget.focusSeedStore,
|
||||
appearance: _appearance,
|
||||
onAppearanceChanged: _setAppearance,
|
||||
language: _language,
|
||||
showLanguageTools: !kReleaseMode && widget.locale == null,
|
||||
onLanguageChanged: _setLanguage,
|
||||
),
|
||||
home:
|
||||
!_preferencesLoaded
|
||||
? const Scaffold(body: SafeArea(child: LoadingView()))
|
||||
: widget.showWelcomeOnFirstRun && !_onboardingComplete
|
||||
? WelcomeScreen(
|
||||
onStart: _completeOnboarding,
|
||||
onSkip: _completeOnboarding,
|
||||
)
|
||||
: FocusSeedHome(
|
||||
focusSeedStore: widget.focusSeedStore,
|
||||
appearance: _appearance,
|
||||
onAppearanceChanged: _setAppearance,
|
||||
language: _language,
|
||||
showLanguageTools: !kReleaseMode && widget.locale == null,
|
||||
onLanguageChanged: _setLanguage,
|
||||
showLandingSetupInitially:
|
||||
widget.showWelcomeOnFirstRun &&
|
||||
(!_landingSetupComplete || _showLandingSetup),
|
||||
onLandingSetupComplete: () {
|
||||
unawaited(_completeLandingSetup());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../core/date_keys.dart';
|
||||
import '../core/focus_durations.dart';
|
||||
import '../data/focus_seed_store.dart';
|
||||
import '../models/app_state.dart';
|
||||
import '../models/focus_session.dart';
|
||||
@@ -55,12 +56,18 @@ class FocusSeedController extends ChangeNotifier {
|
||||
|
||||
Future<bool> setTodayAnchor(String value) async {
|
||||
final current = _state;
|
||||
final anchor = value.trim();
|
||||
if (current == null || anchor.isEmpty) {
|
||||
final title = value.trim();
|
||||
if (current == null || title.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
_analytics.track('intention_saved');
|
||||
return _commit(current.copyWith(anchor: anchor, anchorDateKey: todayKey()));
|
||||
return _commit(
|
||||
current.copyWith(
|
||||
anchor: title,
|
||||
anchorDateKey: todayKey(),
|
||||
selectedActionId: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> toggleHabit(Habit habit) async {
|
||||
@@ -128,46 +135,86 @@ class FocusSeedController extends ChangeNotifier {
|
||||
return _commit(current.copyWith(habits: habits));
|
||||
}
|
||||
|
||||
Future<bool> completeFocusSession(int minutes) async {
|
||||
Future<bool> completeFocusSession(int minutes, {int? actualSeconds}) async {
|
||||
final current = _state;
|
||||
if (current == null) {
|
||||
return false;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final today = todayKey();
|
||||
final anchor = current.activeAnchor(today);
|
||||
final session = FocusSession(
|
||||
id: 'session-${now.microsecondsSinceEpoch}',
|
||||
minutes: minutes,
|
||||
actualSeconds: actualSeconds,
|
||||
completedAt: now,
|
||||
intention: anchor,
|
||||
actionTitleSnapshot: anchor,
|
||||
);
|
||||
_analytics.track('focus_completed', {
|
||||
'minutes': minutes,
|
||||
'actualSeconds': session.actualSeconds,
|
||||
});
|
||||
return _commit(
|
||||
current.copyWith(
|
||||
anchor: anchor,
|
||||
anchorDateKey: today,
|
||||
sessions: [...current.sessions, session],
|
||||
),
|
||||
);
|
||||
_analytics.track('focus_completed', {'minutes': minutes});
|
||||
return _commit(current.copyWith(sessions: [...current.sessions, session]));
|
||||
}
|
||||
|
||||
Future<bool> saveReflection(String value, Mood mood) async {
|
||||
return saveReflectionForDate(todayKey(), value, mood);
|
||||
}
|
||||
|
||||
Future<bool> saveReflectionForDate(
|
||||
String dateKey,
|
||||
String value,
|
||||
Mood mood,
|
||||
) async {
|
||||
final current = _state;
|
||||
final note = value.trim();
|
||||
if (current == null || note.isEmpty) {
|
||||
final trimmedNote = value.trim();
|
||||
final targetDateKey = dateKey.trim();
|
||||
if (current == null || targetDateKey.isEmpty || trimmedNote.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final today = todayKey();
|
||||
final now = DateTime.now();
|
||||
final entry = ReflectionEntry(
|
||||
id: 'reflection-${now.microsecondsSinceEpoch}',
|
||||
dateKey: today,
|
||||
note: note,
|
||||
dateKey: targetDateKey,
|
||||
note: trimmedNote,
|
||||
mood: mood,
|
||||
createdAt: now,
|
||||
);
|
||||
final reflections = [
|
||||
...current.reflections.where((item) => item.dateKey != today),
|
||||
...current.reflections.where((item) => item.dateKey != targetDateKey),
|
||||
entry,
|
||||
];
|
||||
_analytics.track('reflection_saved');
|
||||
return _commit(current.copyWith(reflections: reflections));
|
||||
}
|
||||
|
||||
Future<bool> setDefaultFocusMinutes(int minutes) async {
|
||||
final current = _state;
|
||||
if (current == null || !isFocusDurationPreset(minutes)) {
|
||||
return false;
|
||||
}
|
||||
if (current.defaultFocusMinutes == minutes) {
|
||||
return true;
|
||||
}
|
||||
_analytics.track('default_focus_minutes_changed', {'minutes': minutes});
|
||||
return _commit(current.copyWith(defaultFocusMinutes: minutes));
|
||||
}
|
||||
|
||||
Future<bool> clearData() async {
|
||||
return _commit(AppState.starter(defaultHabitTitles: _defaultHabitTitles));
|
||||
final current = _state;
|
||||
return _commit(
|
||||
AppState.starter(defaultHabitTitles: _defaultHabitTitles).copyWith(
|
||||
defaultFocusMinutes:
|
||||
current?.defaultFocusMinutes ?? defaultFocusDurationMinutes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> clearToday() async {
|
||||
@@ -190,12 +237,22 @@ class FocusSeedController extends ChangeNotifier {
|
||||
final reflections = current.reflections
|
||||
.where((reflection) => reflection.dateKey != today)
|
||||
.toList(growable: false);
|
||||
final actions = current.actions
|
||||
.where((action) => action.dateKey != today)
|
||||
.toList(growable: false);
|
||||
final progressNotes = current.progressNotes
|
||||
.where((note) => note.dateKey != today)
|
||||
.toList(growable: false);
|
||||
_analytics.track('today_cleared');
|
||||
return _commit(
|
||||
current.copyWith(
|
||||
anchor: current.anchorDateKey == today ? '' : current.anchor,
|
||||
selectedActionId:
|
||||
current.anchorDateKey == today ? '' : current.selectedActionId,
|
||||
habits: habits,
|
||||
actions: actions,
|
||||
sessions: sessions,
|
||||
progressNotes: progressNotes,
|
||||
reflections: reflections,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,21 +3,30 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../core/focus_durations.dart';
|
||||
import '../data/focus_timer_session_store.dart';
|
||||
|
||||
enum FocusTimerStatus { idle, running, paused, completed }
|
||||
|
||||
typedef FocusTimerCompleted =
|
||||
Future<bool> Function(int minutes, int actualSeconds);
|
||||
|
||||
class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
FocusTimerController({
|
||||
required Future<void> Function(int minutes) onCompleted,
|
||||
int initialMinutes = 25,
|
||||
required FocusTimerCompleted onCompleted,
|
||||
FocusTimerSessionStore? sessionStore,
|
||||
int initialMinutes = defaultFocusDurationMinutes,
|
||||
}) : _onCompleted = onCompleted,
|
||||
_sessionStore = sessionStore,
|
||||
_selectedMinutes = initialMinutes,
|
||||
_remainingSeconds = initialMinutes * 60 {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
static const presets = [15, 25, 45];
|
||||
static const presets = focusDurationPresets;
|
||||
|
||||
final Future<void> Function(int minutes) _onCompleted;
|
||||
final FocusTimerCompleted _onCompleted;
|
||||
final FocusTimerSessionStore? _sessionStore;
|
||||
Timer? _ticker;
|
||||
DateTime? _endsAt;
|
||||
int _selectedMinutes;
|
||||
@@ -27,6 +36,8 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
|
||||
int get selectedMinutes => _selectedMinutes;
|
||||
int get remainingSeconds => _remainingSeconds;
|
||||
int get elapsedSeconds =>
|
||||
math.max(0, (_selectedMinutes * 60) - _remainingSeconds);
|
||||
FocusTimerStatus get status => _status;
|
||||
bool get isRunning => _status == FocusTimerStatus.running;
|
||||
bool get isPaused => _status == FocusTimerStatus.paused;
|
||||
@@ -38,6 +49,53 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
: (1 - (_remainingSeconds / totalSeconds)).clamp(0, 1);
|
||||
}
|
||||
|
||||
Future<bool> restoreSavedSession() async {
|
||||
final sessionStore = _sessionStore;
|
||||
if (sessionStore == null) {
|
||||
return false;
|
||||
}
|
||||
final snapshot = await sessionStore.load();
|
||||
if (snapshot == null) {
|
||||
return false;
|
||||
}
|
||||
if (!presets.contains(snapshot.selectedMinutes)) {
|
||||
await sessionStore.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
_selectedMinutes = snapshot.selectedMinutes;
|
||||
final totalSeconds = _selectedMinutes * 60;
|
||||
_remainingSeconds =
|
||||
snapshot.remainingSeconds.clamp(0, totalSeconds).toInt();
|
||||
_ticker?.cancel();
|
||||
_endsAt = null;
|
||||
|
||||
switch (snapshot.status) {
|
||||
case FocusTimerSnapshotStatus.running:
|
||||
_status = FocusTimerStatus.running;
|
||||
_endsAt =
|
||||
snapshot.endsAt ??
|
||||
DateTime.now().add(Duration(seconds: _remainingSeconds));
|
||||
_refreshRemainingTime();
|
||||
if (_remainingSeconds <= 0) {
|
||||
unawaited(_complete());
|
||||
} else {
|
||||
_startTicker();
|
||||
}
|
||||
break;
|
||||
case FocusTimerSnapshotStatus.paused:
|
||||
_status = FocusTimerStatus.paused;
|
||||
break;
|
||||
case FocusTimerSnapshotStatus.completed:
|
||||
_remainingSeconds = 0;
|
||||
_status = FocusTimerStatus.completed;
|
||||
unawaited(_retryCompletedSnapshot(snapshot));
|
||||
break;
|
||||
}
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
void selectMinutes(int minutes) {
|
||||
if (isRunning || !presets.contains(minutes)) {
|
||||
return;
|
||||
@@ -58,6 +116,7 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_status = FocusTimerStatus.running;
|
||||
_endsAt = DateTime.now().add(Duration(seconds: _remainingSeconds));
|
||||
_startTicker();
|
||||
unawaited(_persistActiveSession());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -69,6 +128,7 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_ticker?.cancel();
|
||||
_endsAt = null;
|
||||
_status = FocusTimerStatus.paused;
|
||||
unawaited(_persistActiveSession());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -77,6 +137,7 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_endsAt = null;
|
||||
_remainingSeconds = _selectedMinutes * 60;
|
||||
_status = FocusTimerStatus.idle;
|
||||
unawaited(_clearSavedSession());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -118,19 +179,100 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
if (_isCompleting || isCompleted) {
|
||||
return;
|
||||
}
|
||||
if (isRunning) {
|
||||
_refreshRemainingTime();
|
||||
}
|
||||
final totalSeconds = _selectedMinutes * 60;
|
||||
final actualSeconds =
|
||||
_remainingSeconds <= 0
|
||||
? totalSeconds
|
||||
: math.max(1, totalSeconds - _remainingSeconds);
|
||||
_isCompleting = true;
|
||||
_ticker?.cancel();
|
||||
_endsAt = null;
|
||||
_remainingSeconds = 0;
|
||||
_status = FocusTimerStatus.completed;
|
||||
unawaited(_persistCompletedSession(actualSeconds));
|
||||
notifyListeners();
|
||||
try {
|
||||
await _onCompleted(_selectedMinutes);
|
||||
final saved = await _onCompleted(_selectedMinutes, actualSeconds);
|
||||
if (saved) {
|
||||
await _clearSavedSession();
|
||||
}
|
||||
} finally {
|
||||
_isCompleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _retryCompletedSnapshot(
|
||||
FocusTimerSessionSnapshot snapshot,
|
||||
) async {
|
||||
if (_isCompleting || snapshot.actualSeconds == null) {
|
||||
return;
|
||||
}
|
||||
_isCompleting = true;
|
||||
try {
|
||||
final saved = await _onCompleted(
|
||||
snapshot.selectedMinutes,
|
||||
snapshot.actualSeconds!,
|
||||
);
|
||||
if (saved) {
|
||||
await _clearSavedSession();
|
||||
}
|
||||
} finally {
|
||||
_isCompleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistActiveSession() async {
|
||||
final sessionStore = _sessionStore;
|
||||
if (sessionStore == null) {
|
||||
return;
|
||||
}
|
||||
if (isRunning) {
|
||||
await sessionStore.save(
|
||||
FocusTimerSessionSnapshot(
|
||||
status: FocusTimerSnapshotStatus.running,
|
||||
selectedMinutes: _selectedMinutes,
|
||||
remainingSeconds: _remainingSeconds,
|
||||
endsAt: _endsAt,
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isPaused) {
|
||||
await sessionStore.save(
|
||||
FocusTimerSessionSnapshot(
|
||||
status: FocusTimerSnapshotStatus.paused,
|
||||
selectedMinutes: _selectedMinutes,
|
||||
remainingSeconds: _remainingSeconds,
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistCompletedSession(int actualSeconds) async {
|
||||
final sessionStore = _sessionStore;
|
||||
if (sessionStore == null) {
|
||||
return;
|
||||
}
|
||||
await sessionStore.save(
|
||||
FocusTimerSessionSnapshot(
|
||||
status: FocusTimerSnapshotStatus.completed,
|
||||
selectedMinutes: _selectedMinutes,
|
||||
remainingSeconds: 0,
|
||||
actualSeconds: actualSeconds,
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _clearSavedSession() async {
|
||||
await _sessionStore?.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
|
||||
@@ -22,7 +22,7 @@ List<String> lastSevenDateKeys() {
|
||||
int minutesForDate(List<FocusSession> sessions, String key) {
|
||||
return sessions
|
||||
.where((session) => dateKey(session.completedAt) == key)
|
||||
.fold<int>(0, (total, session) => total + session.minutes);
|
||||
.fold<int>(0, (total, session) => total + session.actualMinutes);
|
||||
}
|
||||
|
||||
int currentStreak(
|
||||
|
||||
@@ -3,9 +3,22 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/appearance_preference.dart';
|
||||
import '../models/language_preference.dart';
|
||||
|
||||
class FirstRunProgress {
|
||||
const FirstRunProgress({
|
||||
required this.onboardingComplete,
|
||||
required this.landingSetupComplete,
|
||||
});
|
||||
|
||||
final bool onboardingComplete;
|
||||
final bool landingSetupComplete;
|
||||
}
|
||||
|
||||
class AppPreferencesStore {
|
||||
static const _appearanceKey = 'focus_seed_appearance';
|
||||
static const _languageKey = 'focus_seed_language';
|
||||
static const _legacyWelcomeCompleteKey = 'focus_seed_welcome_complete';
|
||||
static const _onboardingCompleteKey = 'focus_seed_onboarding_complete';
|
||||
static const _landingSetupCompleteKey = 'focus_seed_landing_setup_complete';
|
||||
|
||||
Future<AppearancePreference> loadAppearance() async {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
@@ -26,4 +39,34 @@ class AppPreferencesStore {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
await preferences.setString(_languageKey, language.name);
|
||||
}
|
||||
|
||||
Future<FirstRunProgress> loadFirstRunProgress() async {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
final legacyComplete =
|
||||
preferences.getBool(_legacyWelcomeCompleteKey) ?? false;
|
||||
final landingSetupComplete =
|
||||
preferences.getBool(_landingSetupCompleteKey) ?? legacyComplete;
|
||||
final onboardingComplete =
|
||||
preferences.getBool(_onboardingCompleteKey) ??
|
||||
(legacyComplete || landingSetupComplete);
|
||||
return FirstRunProgress(
|
||||
onboardingComplete: onboardingComplete,
|
||||
landingSetupComplete: landingSetupComplete,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveOnboardingComplete() async {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
await preferences.setBool(_onboardingCompleteKey, true);
|
||||
if (!preferences.containsKey(_landingSetupCompleteKey)) {
|
||||
await preferences.setBool(_landingSetupCompleteKey, false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveLandingSetupComplete() async {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
await preferences.setBool(_onboardingCompleteKey, true);
|
||||
await preferences.setBool(_landingSetupCompleteKey, true);
|
||||
await preferences.setBool(_legacyWelcomeCompleteKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import 'package:drift/drift.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../core/date_keys.dart' as date_keys;
|
||||
import '../core/focus_durations.dart';
|
||||
import '../models/app_state.dart';
|
||||
import '../models/daily_action.dart';
|
||||
import '../models/focus_session.dart';
|
||||
import '../models/habit.dart';
|
||||
import '../models/mood.dart';
|
||||
import '../models/progress_note.dart';
|
||||
import '../models/reflection_entry.dart';
|
||||
import 'local/focus_seed_database.dart';
|
||||
|
||||
@@ -17,6 +20,7 @@ class FocusSeedStore {
|
||||
|
||||
static const _legacyStorageKey = 'focus_seed_state_v1';
|
||||
static const _legacyMigrationKey = 'focus_seed_state_v1_migrated_to_sqlite';
|
||||
static const _defaultFocusMinutesKey = 'default_focus_minutes';
|
||||
|
||||
final FocusSeedDatabase _database;
|
||||
|
||||
@@ -35,25 +39,56 @@ class FocusSeedStore {
|
||||
Future<void> save(AppState state) async {
|
||||
await _database.transaction(() async {
|
||||
await _database.delete(_database.habitLogRows).go();
|
||||
await _database.delete(_database.progressNoteRows).go();
|
||||
await _database.delete(_database.reflectionRows).go();
|
||||
await _database.delete(_database.focusSessionRows).go();
|
||||
await _database.delete(_database.recentLabelRows).go();
|
||||
await _database.delete(_database.dailyActionRows).go();
|
||||
await _database.delete(_database.habitRows).go();
|
||||
await _database.delete(_database.dailyIntentionRows).go();
|
||||
|
||||
if (state.anchor.trim().isNotEmpty) {
|
||||
await _database
|
||||
.into(_database.appSettingRows)
|
||||
.insertOnConflictUpdate(
|
||||
AppSettingRowsCompanion.insert(
|
||||
key: _defaultFocusMinutesKey,
|
||||
value: state.defaultFocusMinutes.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
final anchorText = state.anchor.trim();
|
||||
if (anchorText.isNotEmpty) {
|
||||
final now = DateTime.now();
|
||||
await _database
|
||||
.into(_database.dailyIntentionRows)
|
||||
.insert(
|
||||
DailyIntentionRowsCompanion.insert(
|
||||
dateKey: state.anchorDateKey,
|
||||
intentionText: state.anchor,
|
||||
intentionText: anchorText,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (final action in state.actions) {
|
||||
await _database
|
||||
.into(_database.dailyActionRows)
|
||||
.insert(_dailyActionCompanion(action));
|
||||
}
|
||||
|
||||
for (final label in state.recentLabels) {
|
||||
await _database
|
||||
.into(_database.recentLabelRows)
|
||||
.insert(
|
||||
RecentLabelRowsCompanion.insert(
|
||||
label: label,
|
||||
lastUsedAt: DateTime.now(),
|
||||
useCount: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (final (index, habit) in state.habits.indexed) {
|
||||
await _database
|
||||
.into(_database.habitRows)
|
||||
@@ -85,6 +120,12 @@ class FocusSeedStore {
|
||||
.insert(_focusSessionCompanion(session));
|
||||
}
|
||||
|
||||
for (final note in state.progressNotes) {
|
||||
await _database
|
||||
.into(_database.progressNoteRows)
|
||||
.insert(_progressNoteCompanion(note));
|
||||
}
|
||||
|
||||
for (final reflection in state.reflections) {
|
||||
await _database
|
||||
.into(_database.reflectionRows)
|
||||
@@ -120,6 +161,12 @@ class FocusSeedStore {
|
||||
..limit(1))
|
||||
.get();
|
||||
final latestIntention = intentionRows.firstOrNull;
|
||||
final dailyActionRows =
|
||||
await (_database.select(_database.dailyActionRows)..orderBy([
|
||||
(row) => OrderingTerm.asc(row.dateKey),
|
||||
(row) => OrderingTerm.asc(row.orderIndex),
|
||||
(row) => OrderingTerm.asc(row.createdAt),
|
||||
])).get();
|
||||
final habitRows =
|
||||
await (_database.select(_database.habitRows)
|
||||
..where((row) => row.archivedAt.isNull())
|
||||
@@ -134,18 +181,36 @@ class FocusSeedStore {
|
||||
final focusSessionRows =
|
||||
await (_database.select(_database.focusSessionRows)
|
||||
..orderBy([(row) => OrderingTerm.asc(row.endedAt)])).get();
|
||||
final progressNoteRows =
|
||||
await (_database.select(_database.progressNoteRows)
|
||||
..orderBy([(row) => OrderingTerm.asc(row.createdAt)])).get();
|
||||
final reflectionRows =
|
||||
await (_database.select(_database.reflectionRows)
|
||||
..orderBy([(row) => OrderingTerm.asc(row.createdAt)])).get();
|
||||
final recentLabelRows =
|
||||
await (_database.select(_database.recentLabelRows)..orderBy([
|
||||
(row) => OrderingTerm.desc(row.lastUsedAt),
|
||||
(row) => OrderingTerm.desc(row.useCount),
|
||||
])).get();
|
||||
final defaultFocusMinutes = await _readDefaultFocusMinutes();
|
||||
|
||||
final completedDatesByHabit = <String, Set<String>>{};
|
||||
for (final log in habitLogRows) {
|
||||
completedDatesByHabit.putIfAbsent(log.habitId, () => {}).add(log.dateKey);
|
||||
}
|
||||
|
||||
final actions = [
|
||||
for (final action in dailyActionRows) _dailyActionFromRow(action),
|
||||
];
|
||||
final progressNotes = [
|
||||
for (final note in progressNoteRows) _progressNoteFromRow(note),
|
||||
];
|
||||
final anchorDateKey = latestIntention?.dateKey ?? date_keys.todayKey();
|
||||
final anchor = latestIntention?.intentionText ?? '';
|
||||
|
||||
return AppState(
|
||||
anchor: latestIntention?.intentionText ?? '',
|
||||
anchorDateKey: latestIntention?.dateKey ?? date_keys.todayKey(),
|
||||
anchor: anchor,
|
||||
anchorDateKey: anchorDateKey,
|
||||
habits: [
|
||||
for (final habit in habitRows)
|
||||
Habit(
|
||||
@@ -160,7 +225,12 @@ class FocusSeedStore {
|
||||
FocusSession(
|
||||
id: session.id,
|
||||
minutes: session.plannedMinutes,
|
||||
actualSeconds: session.actualSeconds,
|
||||
completedAt: session.endedAt,
|
||||
intention: session.intentionText ?? '',
|
||||
actionId: session.actionId,
|
||||
actionTitleSnapshot:
|
||||
session.actionTitleSnapshot ?? session.intentionText,
|
||||
),
|
||||
],
|
||||
reflections: [
|
||||
@@ -176,9 +246,28 @@ class FocusSeedStore {
|
||||
createdAt: reflection.createdAt,
|
||||
),
|
||||
],
|
||||
actions: actions,
|
||||
progressNotes: progressNotes,
|
||||
recentLabels: [
|
||||
for (final label in recentLabelRows)
|
||||
if (label.label.trim().isNotEmpty) label.label.trim(),
|
||||
],
|
||||
defaultFocusMinutes: defaultFocusMinutes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> _readDefaultFocusMinutes() async {
|
||||
final row =
|
||||
await (_database.select(_database.appSettingRows)..where(
|
||||
(row) => row.key.equals(_defaultFocusMinutesKey),
|
||||
)).getSingleOrNull();
|
||||
final value = int.tryParse(row?.value ?? '');
|
||||
if (value == null || !isFocusDurationPreset(value)) {
|
||||
return defaultFocusDurationMinutes;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<void> _migrateLegacyJsonIfNeeded() async {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
if (preferences.getBool(_legacyMigrationKey) ?? false) {
|
||||
@@ -206,6 +295,12 @@ class FocusSeedStore {
|
||||
}
|
||||
|
||||
Future<bool> _hasDatabaseContent() async {
|
||||
final action =
|
||||
await (_database.select(_database.dailyActionRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
if (action != null) {
|
||||
return true;
|
||||
}
|
||||
final intention =
|
||||
await (_database.select(_database.dailyIntentionRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
@@ -224,20 +319,106 @@ class FocusSeedStore {
|
||||
if (session != null) {
|
||||
return true;
|
||||
}
|
||||
final progress =
|
||||
await (_database.select(_database.progressNoteRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
if (progress != null) {
|
||||
return true;
|
||||
}
|
||||
final reflection =
|
||||
await (_database.select(_database.reflectionRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
return reflection != null;
|
||||
}
|
||||
|
||||
DailyAction _dailyActionFromRow(DailyActionRow row) {
|
||||
return DailyAction(
|
||||
id: row.id,
|
||||
dateKey: row.dateKey,
|
||||
title: row.title,
|
||||
status: dailyActionStatusFromName(row.status),
|
||||
orderIndex: row.orderIndex,
|
||||
labels: _decodeLabels(row.labelsJson),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
archivedAt: row.archivedAt,
|
||||
source: row.source,
|
||||
);
|
||||
}
|
||||
|
||||
ProgressNote _progressNoteFromRow(ProgressNoteRow row) {
|
||||
return ProgressNote(
|
||||
id: row.id,
|
||||
dateKey: row.dateKey,
|
||||
actionId: row.actionId,
|
||||
sessionId: row.sessionId,
|
||||
note: row.note,
|
||||
outcome: progressOutcomeFromName(row.outcome),
|
||||
labels: _decodeLabels(row.labelsJson),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
DailyActionRowsCompanion _dailyActionCompanion(DailyAction action) {
|
||||
return DailyActionRowsCompanion.insert(
|
||||
id: action.id,
|
||||
dateKey: action.dateKey,
|
||||
title: action.title,
|
||||
status: action.status.name,
|
||||
orderIndex: action.orderIndex,
|
||||
labelsJson: jsonEncode(action.labels),
|
||||
createdAt: action.createdAt,
|
||||
updatedAt: action.updatedAt,
|
||||
archivedAt: Value(action.archivedAt),
|
||||
source: action.source,
|
||||
);
|
||||
}
|
||||
|
||||
ProgressNoteRowsCompanion _progressNoteCompanion(ProgressNote note) {
|
||||
return ProgressNoteRowsCompanion.insert(
|
||||
id: note.id,
|
||||
dateKey: note.dateKey,
|
||||
actionId: Value(note.actionId),
|
||||
sessionId: Value(note.sessionId),
|
||||
note: note.note,
|
||||
outcome: Value(note.outcome?.name),
|
||||
labelsJson: jsonEncode(note.labels),
|
||||
createdAt: note.createdAt,
|
||||
updatedAt: note.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _decodeLabels(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List<Object?>) {
|
||||
return const [];
|
||||
}
|
||||
return [
|
||||
for (final item in decoded)
|
||||
if (item is String && item.trim().isNotEmpty) item.trim(),
|
||||
];
|
||||
} on Object {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
FocusSessionRowsCompanion _focusSessionCompanion(FocusSession session) {
|
||||
final completedAt = session.completedAt;
|
||||
return FocusSessionRowsCompanion.insert(
|
||||
id: session.id,
|
||||
dateKey: date_keys.dateKey(completedAt),
|
||||
actionId: Value(session.actionId),
|
||||
actionTitleSnapshot: Value(
|
||||
session.actionTitle.trim().isEmpty ? null : session.actionTitle,
|
||||
),
|
||||
intentionText: Value(
|
||||
session.actionTitle.trim().isEmpty ? null : session.actionTitle,
|
||||
),
|
||||
plannedMinutes: session.minutes,
|
||||
actualSeconds: session.minutes * 60,
|
||||
startedAt: completedAt.subtract(Duration(minutes: session.minutes)),
|
||||
actualSeconds: session.actualSeconds,
|
||||
startedAt: completedAt.subtract(Duration(seconds: session.actualSeconds)),
|
||||
endedAt: completedAt,
|
||||
status: 'completed',
|
||||
);
|
||||
|
||||
@@ -26,6 +26,10 @@ class FocusSessionRows extends Table {
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get dateKey => text()();
|
||||
TextColumn get actionId => text().nullable().named('action_id')();
|
||||
TextColumn get actionTitleSnapshot =>
|
||||
text().nullable().named('action_title_snapshot')();
|
||||
TextColumn get intentionText => text().nullable().named('intention_text')();
|
||||
IntColumn get plannedMinutes => integer()();
|
||||
IntColumn get actualSeconds => integer()();
|
||||
DateTimeColumn get startedAt => dateTime()();
|
||||
@@ -36,6 +40,55 @@ class FocusSessionRows extends Table {
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class DailyActionRows extends Table {
|
||||
@override
|
||||
String get tableName => 'daily_actions';
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get dateKey => text()();
|
||||
TextColumn get title => text()();
|
||||
TextColumn get status => text()();
|
||||
IntColumn get orderIndex => integer().named('order_index')();
|
||||
TextColumn get labelsJson => text().named('labels_json')();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
DateTimeColumn get archivedAt => dateTime().nullable()();
|
||||
TextColumn get source => text()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class ProgressNoteRows extends Table {
|
||||
@override
|
||||
String get tableName => 'progress_notes';
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get dateKey => text()();
|
||||
TextColumn get actionId => text().nullable().named('action_id')();
|
||||
TextColumn get sessionId => text().nullable().named('session_id')();
|
||||
TextColumn get note => text()();
|
||||
TextColumn get outcome => text().nullable()();
|
||||
TextColumn get labelsJson => text().named('labels_json')();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class RecentLabelRows extends Table {
|
||||
@override
|
||||
String get tableName => 'recent_labels';
|
||||
|
||||
TextColumn get label => text()();
|
||||
DateTimeColumn get lastUsedAt => dateTime()();
|
||||
IntColumn get useCount => integer()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {label};
|
||||
}
|
||||
|
||||
class HabitRows extends Table {
|
||||
@override
|
||||
String get tableName => 'habits';
|
||||
@@ -92,7 +145,10 @@ class AppSettingRows extends Table {
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
DailyIntentionRows,
|
||||
DailyActionRows,
|
||||
FocusSessionRows,
|
||||
ProgressNoteRows,
|
||||
RecentLabelRows,
|
||||
HabitRows,
|
||||
HabitLogRows,
|
||||
ReflectionRows,
|
||||
@@ -104,13 +160,43 @@ class FocusSeedDatabase extends _$FocusSeedDatabase {
|
||||
: super(executor ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 5;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onUpgrade: (migrator, from, to) async {
|
||||
if (from < 3) {
|
||||
await customStatement('DROP TABLE IF EXISTS focus_plans');
|
||||
await customStatement('DROP TABLE IF EXISTS preference_tags');
|
||||
}
|
||||
if (from < 4) {
|
||||
await customStatement(
|
||||
'ALTER TABLE focus_sessions ADD COLUMN intention_text TEXT NULL',
|
||||
);
|
||||
}
|
||||
if (from < 5) {
|
||||
await migrator.createTable(dailyActionRows);
|
||||
await migrator.createTable(progressNoteRows);
|
||||
await migrator.createTable(recentLabelRows);
|
||||
await migrator.addColumn(focusSessionRows, focusSessionRows.actionId);
|
||||
await migrator.addColumn(
|
||||
focusSessionRows,
|
||||
focusSessionRows.actionTitleSnapshot,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearProductData() async {
|
||||
await transaction(() async {
|
||||
await delete(habitLogRows).go();
|
||||
await delete(progressNoteRows).go();
|
||||
await delete(reflectionRows).go();
|
||||
await delete(focusSessionRows).go();
|
||||
await delete(recentLabelRows).go();
|
||||
await delete(dailyActionRows).go();
|
||||
await delete(habitRows).go();
|
||||
await delete(dailyIntentionRows).go();
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../application/focus_seed_controller.dart';
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../widgets/app_bottom_sheet.dart';
|
||||
import '../../widgets/app_empty_state.dart';
|
||||
import '../../widgets/dialogs.dart';
|
||||
import '../../widgets/loading_view.dart';
|
||||
import '../today/widgets/compact_habit_row.dart';
|
||||
|
||||
Future<void> showHabitManagerSheet(
|
||||
@@ -21,89 +25,63 @@ Future<void> showHabitManagerSheet(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
final state = controller.state;
|
||||
if (state == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
if (state == null) {
|
||||
return AppBottomSheetScaffold(
|
||||
title: l10n.habitsTitle,
|
||||
subtitle: l10n.habitsSubtitle,
|
||||
icon: Icons.add_task_rounded,
|
||||
accent: visuals.habit,
|
||||
expand: true,
|
||||
child: const LoadingView(),
|
||||
);
|
||||
}
|
||||
final today = todayKey();
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 12, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.habitsTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(
|
||||
l10n.habitsSubtitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: l10n.addHabit,
|
||||
onPressed:
|
||||
return AppBottomSheetScaffold(
|
||||
title: l10n.habitsTitle,
|
||||
subtitle: l10n.habitsSubtitle,
|
||||
icon: Icons.add_task_rounded,
|
||||
accent: visuals.habit,
|
||||
expand: true,
|
||||
trailing: IconButton.filledTonal(
|
||||
tooltip: l10n.addHabit,
|
||||
onPressed:
|
||||
() => showHabitDialog(context, controller.addHabit),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
),
|
||||
child:
|
||||
state.habits.isEmpty
|
||||
? _EmptyHabits(
|
||||
onAdd:
|
||||
() =>
|
||||
showHabitDialog(context, controller.addHabit),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child:
|
||||
state.habits.isEmpty
|
||||
? _EmptyHabits(
|
||||
onAdd:
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(0, 2, 0, 24),
|
||||
itemCount: state.habits.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final habit = state.habits[index];
|
||||
return CompactHabitRow(
|
||||
habit: habit,
|
||||
dateKey: today,
|
||||
onToggle: () => controller.toggleHabit(habit),
|
||||
onEdit:
|
||||
() => showHabitDialog(
|
||||
context,
|
||||
controller.addHabit,
|
||||
(value) =>
|
||||
controller.renameHabit(habit, value),
|
||||
initialValue: habit.title,
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 32),
|
||||
itemCount: state.habits.length,
|
||||
separatorBuilder: (_, _) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final habit = state.habits[index];
|
||||
return CompactHabitRow(
|
||||
habit: habit,
|
||||
dateKey: today,
|
||||
onToggle: () => controller.toggleHabit(habit),
|
||||
onEdit:
|
||||
() => showHabitDialog(
|
||||
context,
|
||||
(value) => controller.renameHabit(
|
||||
habit,
|
||||
value,
|
||||
),
|
||||
initialValue: habit.title,
|
||||
),
|
||||
onDelete:
|
||||
() => confirmDeleteHabit(
|
||||
context,
|
||||
() => controller.deleteHabit(habit),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
onDelete:
|
||||
() => confirmDeleteHabit(
|
||||
context,
|
||||
() => controller.deleteHabit(habit),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -118,31 +96,17 @@ class _EmptyHabits extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.eco_outlined,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
context.l10n.emptyHabitsTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
FilledButton.icon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(context.l10n.addHabit),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
final l10n = context.l10n;
|
||||
return AppEmptyState(
|
||||
title: l10n.emptyHabitsTitle,
|
||||
body: l10n.emptyHabitPrompt,
|
||||
icon: Icons.add_task_rounded,
|
||||
actionLabel: l10n.addHabit,
|
||||
actionIcon: Icons.add_rounded,
|
||||
onAction: onAdd,
|
||||
accent: FocusSeedVisuals.of(context).habit,
|
||||
illustrationHeight: 86,
|
||||
padding: const EdgeInsets.all(28),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../application/focus_seed_controller.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../models/focus_session.dart';
|
||||
import '../../models/reflection_entry.dart';
|
||||
import '../../widgets/seed_illustration.dart';
|
||||
import '../../widgets/surface_panel.dart';
|
||||
|
||||
enum _HistoryView { focus, notes }
|
||||
|
||||
class HistoryScreen extends StatefulWidget {
|
||||
const HistoryScreen({required this.controller, super.key});
|
||||
|
||||
final FocusSeedController controller;
|
||||
|
||||
@override
|
||||
State<HistoryScreen> createState() => _HistoryScreenState();
|
||||
}
|
||||
|
||||
class _HistoryScreenState extends State<HistoryScreen> {
|
||||
_HistoryView _view = _HistoryView.focus;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.historyTitle)),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: AnimatedBuilder(
|
||||
animation: widget.controller,
|
||||
builder: (context, _) {
|
||||
final state = widget.controller.state!;
|
||||
final focusSessions = state.sessions.reversed.toList();
|
||||
final reflections = state.reflections.reversed.toList();
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 28),
|
||||
children: [
|
||||
_HistorySwitch(
|
||||
value: _view,
|
||||
onChanged: (value) => setState(() => _view = value),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
child:
|
||||
_view == _HistoryView.focus
|
||||
? _FocusHistoryList(
|
||||
key: const ValueKey('focus-history'),
|
||||
sessions: focusSessions,
|
||||
onStartToday: () => Navigator.of(context).pop(),
|
||||
)
|
||||
: _ReflectionHistoryList(
|
||||
key: const ValueKey('reflection-history'),
|
||||
reflections: reflections,
|
||||
onStartToday: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistorySwitch extends StatelessWidget {
|
||||
const _HistorySwitch({required this.value, required this.onChanged});
|
||||
|
||||
final _HistoryView value;
|
||||
final ValueChanged<_HistoryView> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerLow.withValues(alpha: 0.70),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.70),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _HistorySwitchItem(
|
||||
selected: value == _HistoryView.focus,
|
||||
icon: Icons.timer_outlined,
|
||||
label: l10n.focusHistory,
|
||||
onTap: () => onChanged(_HistoryView.focus),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: _HistorySwitchItem(
|
||||
selected: value == _HistoryView.notes,
|
||||
icon: Icons.edit_note_rounded,
|
||||
label: l10n.notesHistory,
|
||||
onTap: () => onChanged(_HistoryView.notes),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistorySwitchItem extends StatelessWidget {
|
||||
const _HistorySwitchItem({
|
||||
required this.selected,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool selected;
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
selected
|
||||
? scheme.primary.withValues(alpha: 0.16)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow:
|
||||
selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: scheme.primary.withValues(alpha: 0.16),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 19,
|
||||
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color:
|
||||
selected ? scheme.onSurface : scheme.onSurfaceVariant,
|
||||
fontWeight: selected ? FontWeight.w900 : FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FocusHistoryList extends StatelessWidget {
|
||||
const _FocusHistoryList({
|
||||
required this.sessions,
|
||||
required this.onStartToday,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<FocusSession> sessions;
|
||||
final VoidCallback onStartToday;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
if (sessions.isEmpty) {
|
||||
return _EmptyHistory(
|
||||
title: l10n.emptyFocusHistoryTitle,
|
||||
body: l10n.emptyFocusHistoryBody,
|
||||
tone: SeedIllustrationTone.archive,
|
||||
onStartToday: onStartToday,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
for (var index = 0; index < sessions.length; index++) ...[
|
||||
if (index > 0) const SizedBox(height: 8),
|
||||
_HistoryTimelineTile(
|
||||
leading: Icons.timer_outlined,
|
||||
title: l10n.focusMinutes(sessions[index].minutes),
|
||||
subtitle: localizedReadableDate(
|
||||
context,
|
||||
sessions[index].completedAt,
|
||||
),
|
||||
trailing: localizedTime(context, sessions[index].completedAt),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReflectionHistoryList extends StatelessWidget {
|
||||
const _ReflectionHistoryList({
|
||||
required this.reflections,
|
||||
required this.onStartToday,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<ReflectionEntry> reflections;
|
||||
final VoidCallback onStartToday;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
if (reflections.isEmpty) {
|
||||
return _EmptyHistory(
|
||||
title: l10n.emptyNotesHistoryTitle,
|
||||
body: l10n.emptyNotesHistoryBody,
|
||||
tone: SeedIllustrationTone.data,
|
||||
onStartToday: onStartToday,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
for (var index = 0; index < reflections.length; index++) ...[
|
||||
if (index > 0) const SizedBox(height: 8),
|
||||
_HistoryTimelineTile(
|
||||
leading: reflections[index].mood.icon,
|
||||
title: reflections[index].note,
|
||||
subtitle: reflections[index].dateKey,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyHistory extends StatelessWidget {
|
||||
const _EmptyHistory({
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.tone,
|
||||
required this.onStartToday,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
final SeedIllustrationTone tone;
|
||||
final VoidCallback onStartToday;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final minHeight = MediaQuery.sizeOf(context).height * 0.62;
|
||||
return SizedBox(
|
||||
height: minHeight.clamp(430, 620).toDouble(),
|
||||
child: Center(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: Alignment.topCenter,
|
||||
radius: 1.1,
|
||||
colors: [
|
||||
scheme.primary.withValues(alpha: 0.16),
|
||||
scheme.surfaceContainerLowest.withValues(alpha: 0.76),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 26, 18, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SeedIllustration(size: 142, tone: tone, hero: true),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 310),
|
||||
child: Text(
|
||||
body,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
FilledButton.icon(
|
||||
onPressed: onStartToday,
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
label: Text(context.l10n.navToday),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryTimelineTile extends StatelessWidget {
|
||||
const _HistoryTimelineTile({
|
||||
required this.leading,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
final IconData leading;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
return SurfacePanel(
|
||||
padding: const EdgeInsets.fromLTRB(13, 12, 14, 12),
|
||||
borderColor: scheme.outlineVariant.withValues(alpha: 0.60),
|
||||
child: Row(
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: SizedBox.square(
|
||||
dimension: 42,
|
||||
child: Icon(leading, color: scheme.primary, size: 21),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
trailing!,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../application/focus_seed_controller.dart';
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../models/journey_stats.dart';
|
||||
import '../../models/today_progress.dart';
|
||||
import '../../widgets/focus_seed_header.dart';
|
||||
import '../../widgets/seed_illustration.dart';
|
||||
import '../../widgets/surface_panel.dart';
|
||||
import '../reflection/reflection_sheet.dart';
|
||||
import 'history_screen.dart';
|
||||
import 'widgets/growth_week_strip.dart';
|
||||
import 'widgets/journey_overview_panel.dart';
|
||||
import 'widgets/journey_stats_panel.dart';
|
||||
|
||||
class JourneyScreen extends StatelessWidget {
|
||||
const JourneyScreen({required this.controller, super.key});
|
||||
|
||||
final FocusSeedController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = controller.state!;
|
||||
final l10n = context.l10n;
|
||||
final now = DateTime.now();
|
||||
final progress = TodayProgress.fromState(state, todayKey());
|
||||
final stats = JourneyStats.fromState(state);
|
||||
final recentSessions = state.sessions.reversed.take(5).toList();
|
||||
final recentReflections = state.reflections.reversed.take(3).toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
FocusSeedHeader(
|
||||
title: l10n.journeyTitle,
|
||||
subtitle: localizedReadableDate(context, now),
|
||||
isSaving: controller.isSaving,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 6, 20, 32),
|
||||
children: [
|
||||
JourneyOverviewPanel(
|
||||
progress: progress,
|
||||
onReflect: () => showReflectionSheet(context, controller),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
JourneyStatsPanel(stats: stats),
|
||||
const SizedBox(height: 12),
|
||||
GrowthWeekStrip(state: state),
|
||||
const SizedBox(height: 12),
|
||||
_HistoryGateway(
|
||||
onTap:
|
||||
() => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder:
|
||||
(context) => HistoryScreen(controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (recentSessions.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
_SectionTitle(
|
||||
title: l10n.recentFocus,
|
||||
detail: l10n.sessionsTotal(state.sessions.length),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SurfacePanel(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (
|
||||
var index = 0;
|
||||
index < recentSessions.length;
|
||||
index++
|
||||
) ...[
|
||||
if (index > 0) const Divider(),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: Text(
|
||||
l10n.focusMinutes(recentSessions[index].minutes),
|
||||
),
|
||||
subtitle: Text(
|
||||
localizedReadableDate(
|
||||
context,
|
||||
recentSessions[index].completedAt,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text(
|
||||
localizedTime(
|
||||
context,
|
||||
recentSessions[index].completedAt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (recentReflections.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
_SectionTitle(
|
||||
title: l10n.recentReflections,
|
||||
detail: l10n.recentReflectionsDetail,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SurfacePanel(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (
|
||||
var index = 0;
|
||||
index < recentReflections.length;
|
||||
index++
|
||||
) ...[
|
||||
if (index > 0) const Divider(),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(recentReflections[index].mood.icon),
|
||||
title: Text(
|
||||
recentReflections[index].note,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(recentReflections[index].dateKey),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryGateway extends StatelessWidget {
|
||||
const _HistoryGateway({required this.onTap});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
return SurfacePanel(
|
||||
elevated: true,
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 14, 14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
scheme.secondary.withValues(alpha: 0.10),
|
||||
scheme.surfaceContainerLowest,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SeedIllustration(
|
||||
size: 68,
|
||||
tone: SeedIllustrationTone.archive,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.viewHistory,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
l10n.viewHistorySubtitle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 17,
|
||||
color: scheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionTitle extends StatelessWidget {
|
||||
const _SectionTitle({required this.title, required this.detail});
|
||||
|
||||
final String title;
|
||||
final String detail;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
Text(
|
||||
detail,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/date_keys.dart';
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/app_state.dart';
|
||||
import '../../../models/growth_stage.dart';
|
||||
import '../../../widgets/surface_panel.dart';
|
||||
|
||||
class GrowthWeekStrip extends StatelessWidget {
|
||||
const GrowthWeekStrip({required this.state, super.key});
|
||||
|
||||
final AppState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final days = lastSevenDateKeys();
|
||||
final l10n = context.l10n;
|
||||
return SurfacePanel(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.sevenDayGrowth,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.sevenDayGrowthSubtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
for (final day in days)
|
||||
Expanded(
|
||||
child: _GrowthDay(
|
||||
day: day,
|
||||
stage: growthStageFor(state, day),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GrowthDay extends StatelessWidget {
|
||||
const _GrowthDay({required this.day, required this.stage});
|
||||
|
||||
final String day;
|
||||
final GrowthStage stage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = context.l10n;
|
||||
final icon = switch (stage) {
|
||||
GrowthStage.seed => Icons.circle_outlined,
|
||||
GrowthStage.sprout => Icons.grass_rounded,
|
||||
GrowthStage.leaves => Icons.eco_rounded,
|
||||
GrowthStage.flower => Icons.local_florist_rounded,
|
||||
};
|
||||
return Semantics(
|
||||
label: l10n.growthDaySemantic(
|
||||
day.substring(5),
|
||||
localizedGrowthStage(l10n, stage),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
stage == GrowthStage.seed
|
||||
? theme.colorScheme.surfaceContainer
|
||||
: theme.colorScheme.primary.withValues(alpha: 0.14),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 19,
|
||||
color:
|
||||
stage == GrowthStage.seed
|
||||
? theme.colorScheme.onSurfaceVariant
|
||||
: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(day.substring(8), style: theme.textTheme.labelSmall),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/today_progress.dart';
|
||||
import '../../../widgets/seed_illustration.dart';
|
||||
import '../../../widgets/surface_panel.dart';
|
||||
|
||||
class JourneyOverviewPanel extends StatelessWidget {
|
||||
const JourneyOverviewPanel({
|
||||
required this.progress,
|
||||
required this.onReflect,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TodayProgress progress;
|
||||
final VoidCallback onReflect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final completed = progress.completedSteps;
|
||||
final progressValue = completed / 4;
|
||||
final title = switch (completed) {
|
||||
0 => l10n.journeyOverviewStartTitle,
|
||||
4 => l10n.journeyOverviewDoneTitle,
|
||||
_ => l10n.journeyOverviewActiveTitle,
|
||||
};
|
||||
final subtitle = switch (completed) {
|
||||
0 => l10n.journeyOverviewStartBody,
|
||||
4 => l10n.journeyOverviewDoneBody,
|
||||
_ => l10n.journeyOverviewActiveBody,
|
||||
};
|
||||
final showReflectAction =
|
||||
progress.hasIntention && progress.hasFocus && !progress.hasReflection;
|
||||
|
||||
return SurfacePanel(
|
||||
elevated: true,
|
||||
padding: EdgeInsets.zero,
|
||||
borderColor: scheme.outlineVariant.withValues(alpha: 0.45),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
scheme.primary.withValues(alpha: 0.20),
|
||||
scheme.surfaceContainerLowest.withValues(alpha: 0.96),
|
||||
scheme.secondary.withValues(alpha: 0.14),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textScale = MediaQuery.textScalerOf(context).scale(14) / 14;
|
||||
final compact = constraints.maxWidth < 360 || textScale > 1.25;
|
||||
final textContent = _OverviewText(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
completed: completed,
|
||||
progressValue: progressValue,
|
||||
showReflectAction: showReflectAction,
|
||||
onReflect: onReflect,
|
||||
);
|
||||
if (compact) {
|
||||
return Stack(
|
||||
children: [
|
||||
const Positioned(
|
||||
right: -18,
|
||||
top: -20,
|
||||
child: Opacity(
|
||||
opacity: 0.58,
|
||||
child: SeedIllustration(size: 128, hero: true),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 17, 106, 18),
|
||||
child: textContent,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -28,
|
||||
top: -22,
|
||||
bottom: -18,
|
||||
child: Opacity(
|
||||
opacity: 0.96,
|
||||
child: SeedIllustration(
|
||||
size: constraints.maxWidth.clamp(146, 184).toDouble(),
|
||||
hero: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 92,
|
||||
bottom: 18,
|
||||
child: _ProgressBadge(completed: completed),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 17, 168, 18),
|
||||
child: textContent,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverviewText extends StatelessWidget {
|
||||
const _OverviewText({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.completed,
|
||||
required this.progressValue,
|
||||
required this.showReflectAction,
|
||||
required this.onReflect,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final int completed;
|
||||
final double progressValue;
|
||||
final bool showReflectAction;
|
||||
final VoidCallback onReflect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = context.l10n;
|
||||
final scheme = theme.colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.journeyOverviewLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: scheme.primary,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_ProgressSeeds(completed: completed, progressValue: progressValue),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.journeyOverviewProgress(completed),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
if (showReflectAction)
|
||||
TextButton.icon(
|
||||
onPressed: onReflect,
|
||||
icon: const Icon(Icons.edit_note_rounded, size: 18),
|
||||
label: Text(l10n.reviewNow),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressSeeds extends StatelessWidget {
|
||||
const _ProgressSeeds({required this.completed, required this.progressValue});
|
||||
|
||||
final int completed;
|
||||
final double progressValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Semantics(
|
||||
value: '${(progressValue * 100).round()}%',
|
||||
child: Row(
|
||||
children: List.generate(4, (index) {
|
||||
final filled = index < completed;
|
||||
return Expanded(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
height: 10,
|
||||
margin: EdgeInsets.only(right: index == 3 ? 0 : 7),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
filled
|
||||
? scheme.primary
|
||||
: scheme.surfaceContainerHigh.withValues(alpha: 0.82),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
boxShadow:
|
||||
filled
|
||||
? [
|
||||
BoxShadow(
|
||||
color: scheme.primary.withValues(alpha: 0.28),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressBadge extends StatelessWidget {
|
||||
const _ProgressBadge({required this.completed});
|
||||
|
||||
final int completed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final theme = Theme.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerLowest.withValues(alpha: 0.82),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
border: Border.all(
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.65),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.10),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 7),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 7),
|
||||
child: Text(
|
||||
'$completed/4',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: scheme.primary,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/journey_stats.dart';
|
||||
import '../../../widgets/surface_panel.dart';
|
||||
|
||||
class JourneyStatsPanel extends StatelessWidget {
|
||||
const JourneyStatsPanel({required this.stats, super.key});
|
||||
|
||||
final JourneyStats stats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return SurfacePanel(
|
||||
elevated: true,
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.journeyStatsTitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columns = constraints.maxWidth > 520 ? 4 : 2;
|
||||
return GridView.count(
|
||||
crossAxisCount: columns,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: columns == 4 ? 2.05 : 2.15,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
children: [
|
||||
_StatItem(
|
||||
icon: Icons.timer_outlined,
|
||||
animatedTarget: stats.weekFocusMinutes,
|
||||
valueBuilder: (value) => value.toString(),
|
||||
label: l10n.weekFocusLabel,
|
||||
),
|
||||
_StatItem(
|
||||
icon: Icons.local_fire_department_outlined,
|
||||
animatedTarget: stats.currentStreakDays,
|
||||
valueBuilder: l10n.daysValue,
|
||||
label: l10n.streakLabel,
|
||||
),
|
||||
_StatItem(
|
||||
icon: Icons.calendar_month_outlined,
|
||||
animatedTarget: stats.activeDaysThisWeek,
|
||||
valueBuilder: l10n.activeDaysValue,
|
||||
label: l10n.activeDaysLabel,
|
||||
),
|
||||
_StatItem(
|
||||
icon: Icons.check_circle_outline_rounded,
|
||||
animatedTarget: (stats.habitCompletionRate * 100).round(),
|
||||
valueBuilder: (value) => '$value%',
|
||||
label: l10n.habitRateLabel,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatItem extends StatelessWidget {
|
||||
const _StatItem({
|
||||
required this.icon,
|
||||
required this.animatedTarget,
|
||||
required this.valueBuilder,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final int animatedTarget;
|
||||
final String Function(int value) valueBuilder;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primary.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerLowest.withValues(alpha: 0.72),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, color: scheme.primary, size: 19),
|
||||
),
|
||||
const SizedBox(width: 9),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(end: animatedTarget.toDouble()),
|
||||
duration: const Duration(milliseconds: 280),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder:
|
||||
(context, animatedValue, _) => Text(
|
||||
valueBuilder(animatedValue.round()),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../app/app_theme.dart';
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/today_progress.dart';
|
||||
import '../../../widgets/surface_panel.dart';
|
||||
|
||||
class JourneyTimeline extends StatelessWidget {
|
||||
const JourneyTimeline({
|
||||
required this.progress,
|
||||
required this.onReflect,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TodayProgress progress;
|
||||
final VoidCallback onReflect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final items = [
|
||||
_TimelineData(
|
||||
title: l10n.progressIntention,
|
||||
detail:
|
||||
progress.hasIntention ? progress.intention : l10n.chooseOneOutcome,
|
||||
icon: Icons.flag_outlined,
|
||||
complete: progress.hasIntention,
|
||||
),
|
||||
_TimelineData(
|
||||
title: l10n.progressFocus,
|
||||
detail:
|
||||
progress.hasFocus
|
||||
? l10n.focusedMinutes(progress.focusMinutes)
|
||||
: l10n.noFocusSessionYet,
|
||||
icon: Icons.timer_outlined,
|
||||
complete: progress.hasFocus,
|
||||
),
|
||||
_TimelineData(
|
||||
title: l10n.progressHabits,
|
||||
detail: l10n.habitsCompleted(
|
||||
progress.completedHabits,
|
||||
progress.totalHabits,
|
||||
),
|
||||
icon: Icons.eco_outlined,
|
||||
complete: progress.hasHabits,
|
||||
),
|
||||
_TimelineData(
|
||||
title: l10n.progressReflect,
|
||||
detail:
|
||||
progress.hasReflection
|
||||
? l10n.reflectionCaptured
|
||||
: l10n.reflectionAction,
|
||||
icon: Icons.auto_awesome_outlined,
|
||||
complete: progress.hasReflection,
|
||||
action: progress.hasReflection ? null : onReflect,
|
||||
),
|
||||
_TimelineData(
|
||||
title: l10n.growthTitle,
|
||||
detail:
|
||||
progress.hasReflection ? l10n.growthFlowerOpen : l10n.growthLocked,
|
||||
icon: Icons.local_florist_outlined,
|
||||
complete: progress.hasReflection,
|
||||
),
|
||||
];
|
||||
return Column(
|
||||
children: [
|
||||
for (var index = 0; index < items.length; index++)
|
||||
_TimelineItem(data: items[index], isLast: index == items.length - 1),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimelineItem extends StatelessWidget {
|
||||
const _TimelineItem({required this.data, required this.isLast});
|
||||
|
||||
final _TimelineData data;
|
||||
final bool isLast;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final accent =
|
||||
data.icon == Icons.auto_awesome_outlined
|
||||
? visuals.reflection
|
||||
: theme.colorScheme.primary;
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 38,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
data.complete
|
||||
? accent
|
||||
: theme.colorScheme.surfaceContainerLowest,
|
||||
border: Border.all(color: accent),
|
||||
),
|
||||
child: Icon(
|
||||
data.complete ? Icons.check_rounded : data.icon,
|
||||
size: 16,
|
||||
color: data.complete ? theme.colorScheme.onPrimary : accent,
|
||||
),
|
||||
),
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 2,
|
||||
color:
|
||||
data.complete
|
||||
? accent.withValues(alpha: 0.55)
|
||||
: theme.colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: SurfacePanel(
|
||||
padding: const EdgeInsets.all(14),
|
||||
borderColor:
|
||||
data.action == null ? null : accent.withValues(alpha: 0.5),
|
||||
onTap: data.action,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(data.title, style: theme.textTheme.labelLarge),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
data.detail,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (data.action != null)
|
||||
Icon(Icons.arrow_forward_rounded, color: accent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimelineData {
|
||||
const _TimelineData({
|
||||
required this.title,
|
||||
required this.detail,
|
||||
required this.icon,
|
||||
required this.complete,
|
||||
this.action,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String detail;
|
||||
final IconData icon;
|
||||
final bool complete;
|
||||
final VoidCallback? action;
|
||||
}
|
||||
+947
-557
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../application/focus_seed_controller.dart';
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../core/iterable_extensions.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../models/mood.dart';
|
||||
import '../../widgets/app_bottom_sheet.dart';
|
||||
import '../../widgets/app_busy_indicator.dart';
|
||||
import '../../widgets/surface_panel.dart';
|
||||
|
||||
Future<void> showReflectionSheet(
|
||||
BuildContext context,
|
||||
FocusSeedController controller,
|
||||
) {
|
||||
FocusSeedController controller, {
|
||||
String? dateKey,
|
||||
String contextTitle = '',
|
||||
}) {
|
||||
final state = controller.state;
|
||||
if (state == null) {
|
||||
return Future.value();
|
||||
}
|
||||
final existing =
|
||||
final targetDateKey = dateKey ?? todayKey();
|
||||
final title = contextTitle.trim();
|
||||
final existingReflection =
|
||||
state.reflections
|
||||
.where((entry) => entry.dateKey == todayKey())
|
||||
.where((entry) => entry.dateKey == targetDateKey)
|
||||
.firstOrNull;
|
||||
final legacyProgress =
|
||||
state.progressNotes
|
||||
.where((entry) => entry.dateKey == targetDateKey)
|
||||
.firstOrNull;
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
@@ -24,9 +36,15 @@ Future<void> showReflectionSheet(
|
||||
useSafeArea: true,
|
||||
builder:
|
||||
(context) => _ReflectionComposer(
|
||||
initialNote: existing?.note ?? '',
|
||||
initialMood: existing?.mood ?? Mood.clear,
|
||||
onSave: controller.saveReflection,
|
||||
initialNote: existingReflection?.note ?? legacyProgress?.note ?? '',
|
||||
contextTitle: title,
|
||||
dateKey: targetDateKey,
|
||||
onSave:
|
||||
(note) => controller.saveReflectionForDate(
|
||||
targetDateKey,
|
||||
note,
|
||||
existingReflection?.mood ?? Mood.clear,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -34,13 +52,15 @@ Future<void> showReflectionSheet(
|
||||
class _ReflectionComposer extends StatefulWidget {
|
||||
const _ReflectionComposer({
|
||||
required this.initialNote,
|
||||
required this.initialMood,
|
||||
required this.contextTitle,
|
||||
required this.dateKey,
|
||||
required this.onSave,
|
||||
});
|
||||
|
||||
final String initialNote;
|
||||
final Mood initialMood;
|
||||
final Future<bool> Function(String note, Mood mood) onSave;
|
||||
final String contextTitle;
|
||||
final String dateKey;
|
||||
final Future<bool> Function(String note) onSave;
|
||||
|
||||
@override
|
||||
State<_ReflectionComposer> createState() => _ReflectionComposerState();
|
||||
@@ -48,7 +68,6 @@ class _ReflectionComposer extends StatefulWidget {
|
||||
|
||||
class _ReflectionComposerState extends State<_ReflectionComposer> {
|
||||
late final TextEditingController _controller;
|
||||
late Mood _mood;
|
||||
bool _saving = false;
|
||||
String? _errorText;
|
||||
|
||||
@@ -56,7 +75,6 @@ class _ReflectionComposerState extends State<_ReflectionComposer> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialNote);
|
||||
_mood = widget.initialMood;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -75,7 +93,7 @@ class _ReflectionComposerState extends State<_ReflectionComposer> {
|
||||
_saving = true;
|
||||
_errorText = null;
|
||||
});
|
||||
final saved = await widget.onSave(_controller.text, _mood);
|
||||
final saved = await widget.onSave(_controller.text);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@@ -92,72 +110,112 @@ class _ReflectionComposerState extends State<_ReflectionComposer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20,
|
||||
4,
|
||||
20,
|
||||
MediaQuery.viewInsetsOf(context).bottom + 24,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.reflectionSheetTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.reflectionSheetSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
return AppBottomSheetScaffold(
|
||||
title: l10n.reflectionSheetTitle,
|
||||
subtitle: l10n.reflectionSheetSubtitle,
|
||||
icon: Icons.edit_note_rounded,
|
||||
accent: visuals.reflection,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.contextTitle.isNotEmpty) ...[
|
||||
SurfacePanel(
|
||||
radius: 18,
|
||||
padding: const EdgeInsets.fromLTRB(12, 11, 12, 11),
|
||||
color: Color.alphaBlend(
|
||||
visuals.action.withValues(alpha: 0.045),
|
||||
scheme.surfaceContainerLowest,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.flag_outlined, color: visuals.action, size: 19),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.todayIntentionLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: visuals.action,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
widget.contextTitle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final mood in Mood.values)
|
||||
ChoiceChip(
|
||||
avatar: Icon(mood.icon, size: 17),
|
||||
label: Text(localizedMood(l10n, mood)),
|
||||
selected: _mood == mood,
|
||||
onSelected: (_) => setState(() => _mood = mood),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextField(
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
SurfacePanel(
|
||||
radius: 18,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
|
||||
tone: SurfacePanelTone.paper,
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
autofocus: widget.initialNote.isEmpty,
|
||||
minLines: 4,
|
||||
maxLines: 7,
|
||||
maxLength: 280,
|
||||
scrollPadding: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewInsetsOf(context).bottom + 32,
|
||||
),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: l10n.reflectionHint,
|
||||
errorText: _errorText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon:
|
||||
_saving
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.auto_awesome_outlined),
|
||||
label: Text(_saving ? l10n.savingLabel : l10n.saveReflection),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon:
|
||||
_saving
|
||||
? AppBusyIndicator(semanticLabel: l10n.savingLabel)
|
||||
: const Icon(Icons.auto_awesome_outlined),
|
||||
label: Text(_saving ? l10n.savingLabel : l10n.saveReflection),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: visuals.reflection,
|
||||
foregroundColor:
|
||||
ThemeData.estimateBrightnessForColor(visuals.reflection) ==
|
||||
Brightness.dark
|
||||
? Colors.white
|
||||
: scheme.onSurface,
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
textStyle: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../application/focus_seed_controller.dart';
|
||||
import '../../application/focus_timer_controller.dart';
|
||||
import '../../data/focus_seed_store.dart';
|
||||
import '../../data/focus_timer_session_store.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../models/appearance_preference.dart';
|
||||
import '../../models/language_preference.dart';
|
||||
import '../../widgets/loading_view.dart';
|
||||
import '../journey/journey_screen.dart';
|
||||
import '../../widgets/tactile_canvas.dart';
|
||||
import '../focus/focus_screen.dart';
|
||||
import '../me/me_screen.dart';
|
||||
import '../onboarding/landing_setup_screen.dart';
|
||||
import '../records/records_screen.dart';
|
||||
import '../today/today_screen.dart';
|
||||
|
||||
class FocusSeedHome extends StatefulWidget {
|
||||
@@ -20,6 +27,8 @@ class FocusSeedHome extends StatefulWidget {
|
||||
required this.showLanguageTools,
|
||||
required this.onLanguageChanged,
|
||||
this.focusSeedStore,
|
||||
this.showLandingSetupInitially = false,
|
||||
this.onLandingSetupComplete,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -29,6 +38,8 @@ class FocusSeedHome extends StatefulWidget {
|
||||
final bool showLanguageTools;
|
||||
final ValueChanged<LanguagePreference> onLanguageChanged;
|
||||
final FocusSeedStore? focusSeedStore;
|
||||
final bool showLandingSetupInitially;
|
||||
final VoidCallback? onLandingSetupComplete;
|
||||
|
||||
@override
|
||||
State<FocusSeedHome> createState() => _FocusSeedHomeState();
|
||||
@@ -38,6 +49,13 @@ class _FocusSeedHomeState extends State<FocusSeedHome> {
|
||||
FocusSeedController? _controller;
|
||||
FocusTimerController? _timerController;
|
||||
int _tabIndex = 0;
|
||||
late bool _landingSetupVisible;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_landingSetupVisible = widget.showLandingSetupInitially;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -50,14 +68,41 @@ class _FocusSeedHomeState extends State<FocusSeedHome> {
|
||||
defaultHabitTitles: localizedStarterHabits(context.l10n),
|
||||
);
|
||||
_timerController = FocusTimerController(
|
||||
onCompleted: (minutes) async {
|
||||
final saved = await _controller!.completeFocusSession(minutes);
|
||||
sessionStore: FocusTimerSessionStore(),
|
||||
onCompleted: (minutes, actualSeconds) async {
|
||||
final saved = await _controller!.completeFocusSession(
|
||||
minutes,
|
||||
actualSeconds: actualSeconds,
|
||||
);
|
||||
if (saved) {
|
||||
await HapticFeedback.mediumImpact();
|
||||
}
|
||||
return saved;
|
||||
},
|
||||
);
|
||||
_controller!.load();
|
||||
unawaited(_loadState());
|
||||
}
|
||||
|
||||
Future<void> _loadState() async {
|
||||
final controller = _controller;
|
||||
final timerController = _timerController;
|
||||
if (controller == null || timerController == null) {
|
||||
return;
|
||||
}
|
||||
await controller.load();
|
||||
final defaultMinutes = controller.state?.defaultFocusMinutes;
|
||||
if (!mounted || defaultMinutes == null) {
|
||||
return;
|
||||
}
|
||||
final restored = await timerController.restoreSavedSession();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (restored) {
|
||||
setState(() => _tabIndex = 1);
|
||||
return;
|
||||
}
|
||||
timerController.selectMinutes(defaultMinutes);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -109,63 +154,101 @@ class _FocusSeedHomeState extends State<FocusSeedHome> {
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness:
|
||||
isDark ? Brightness.light : Brightness.dark,
|
||||
systemNavigationBarColor:
|
||||
theme.navigationBarTheme.backgroundColor ??
|
||||
theme.colorScheme.surface,
|
||||
systemNavigationBarIconBrightness:
|
||||
isDark ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: IndexedStack(
|
||||
index: _tabIndex,
|
||||
children: [
|
||||
TodayScreen(
|
||||
controller: controller,
|
||||
timerController: timerController,
|
||||
),
|
||||
JourneyScreen(controller: controller),
|
||||
MeScreen(
|
||||
controller: controller,
|
||||
appearance: widget.appearance,
|
||||
onAppearanceChanged: widget.onAppearanceChanged,
|
||||
language: widget.language,
|
||||
showLanguageTools: widget.showLanguageTools,
|
||||
onLanguageChanged: widget.onLanguageChanged,
|
||||
),
|
||||
],
|
||||
void openMe() => setState(() => _tabIndex = 3);
|
||||
void completeLandingSetup({bool openFocus = false}) {
|
||||
widget.onLandingSetupComplete?.call();
|
||||
setState(() {
|
||||
_landingSetupVisible = false;
|
||||
if (openFocus) {
|
||||
_tabIndex = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: timerController,
|
||||
builder: (context, _) {
|
||||
final hideBottomNavigation =
|
||||
!_landingSetupVisible &&
|
||||
_tabIndex == 1 &&
|
||||
(timerController.isRunning || timerController.isPaused);
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness:
|
||||
isDark ? Brightness.light : Brightness.dark,
|
||||
systemNavigationBarColor:
|
||||
theme.navigationBarTheme.backgroundColor ??
|
||||
theme.colorScheme.surface,
|
||||
systemNavigationBarIconBrightness:
|
||||
isDark ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _tabIndex,
|
||||
onDestinationSelected: (index) {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() => _tabIndex = index);
|
||||
},
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.eco_outlined),
|
||||
selectedIcon: const Icon(Icons.eco_rounded),
|
||||
label: context.l10n.navToday,
|
||||
child: Scaffold(
|
||||
body: TactileCanvas(
|
||||
child:
|
||||
_landingSetupVisible
|
||||
? LandingSetupScreen(
|
||||
controller: controller,
|
||||
timerController: timerController,
|
||||
onDone: completeLandingSetup,
|
||||
onStartFocus:
|
||||
() => completeLandingSetup(openFocus: true),
|
||||
)
|
||||
: SafeArea(
|
||||
child: IndexedStack(
|
||||
index: _tabIndex,
|
||||
children: [
|
||||
TodayScreen(
|
||||
controller: controller,
|
||||
timerController: timerController,
|
||||
onOpenSettings: openMe,
|
||||
onOpenFocus:
|
||||
() => setState(() => _tabIndex = 1),
|
||||
),
|
||||
FocusScreen(
|
||||
controller: controller,
|
||||
timerController: timerController,
|
||||
onBackToday:
|
||||
() => setState(() => _tabIndex = 0),
|
||||
),
|
||||
RecordsScreen(
|
||||
controller: controller,
|
||||
onStartToday:
|
||||
() => setState(() => _tabIndex = 0),
|
||||
),
|
||||
MeScreen(
|
||||
controller: controller,
|
||||
appearance: widget.appearance,
|
||||
onAppearanceChanged:
|
||||
widget.onAppearanceChanged,
|
||||
language: widget.language,
|
||||
showLanguageTools: widget.showLanguageTools,
|
||||
onLanguageChanged: widget.onLanguageChanged,
|
||||
onDefaultFocusMinutesChanged: (minutes) {
|
||||
if (!timerController.isRunning &&
|
||||
!timerController.isPaused) {
|
||||
timerController.selectMinutes(minutes);
|
||||
}
|
||||
},
|
||||
embedded: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.landscape_outlined),
|
||||
selectedIcon: const Icon(Icons.landscape_rounded),
|
||||
label: context.l10n.navJourney,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.person_outline_rounded),
|
||||
selectedIcon: const Icon(Icons.person_rounded),
|
||||
label: context.l10n.navMe,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar:
|
||||
_landingSetupVisible || hideBottomNavigation
|
||||
? null
|
||||
: _FocusSeedNavigationBar(
|
||||
selectedIndex: _tabIndex,
|
||||
onDestinationSelected: (index) {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() => _tabIndex = index);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -179,6 +262,168 @@ class _FocusSeedHomeState extends State<FocusSeedHome> {
|
||||
}
|
||||
}
|
||||
|
||||
class _FocusSeedNavigationBar extends StatelessWidget {
|
||||
const _FocusSeedNavigationBar({
|
||||
required this.selectedIndex,
|
||||
required this.onDestinationSelected,
|
||||
});
|
||||
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final l10n = context.l10n;
|
||||
final destinations = [
|
||||
_FocusSeedNavigationDestination(
|
||||
key: 'today',
|
||||
icon: Icons.home_outlined,
|
||||
label: l10n.navToday,
|
||||
),
|
||||
_FocusSeedNavigationDestination(
|
||||
key: 'focus',
|
||||
icon: Icons.timer_outlined,
|
||||
label: l10n.progressFocus,
|
||||
),
|
||||
_FocusSeedNavigationDestination(
|
||||
key: 'records',
|
||||
icon: Icons.library_books_outlined,
|
||||
label: l10n.navRecords,
|
||||
),
|
||||
_FocusSeedNavigationDestination(
|
||||
key: 'me',
|
||||
icon: Icons.person_outline_rounded,
|
||||
label: l10n.navMe,
|
||||
),
|
||||
];
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerLowest.withValues(alpha: 0.98),
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.58),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 72,
|
||||
child: Row(
|
||||
children: [
|
||||
for (var index = 0; index < destinations.length; index++)
|
||||
Expanded(
|
||||
child: _FocusSeedNavigationItem(
|
||||
destination: destinations[index],
|
||||
selected: index == selectedIndex,
|
||||
onTap: () => onDestinationSelected(index),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FocusSeedNavigationDestination {
|
||||
const _FocusSeedNavigationDestination({
|
||||
required this.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final IconData icon;
|
||||
final String label;
|
||||
}
|
||||
|
||||
class _FocusSeedNavigationItem extends StatelessWidget {
|
||||
const _FocusSeedNavigationItem({
|
||||
required this.destination,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final _FocusSeedNavigationDestination destination;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final selectedColor = visuals.focus;
|
||||
final inactiveColor =
|
||||
isDark ? theme.colorScheme.onSurfaceVariant : const Color(0xFF4B5563);
|
||||
final foregroundColor = selected ? selectedColor : inactiveColor;
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
label: destination.label,
|
||||
child: InkWell(
|
||||
key: ValueKey('focus-seed-nav-${destination.key}'),
|
||||
onTap: onTap,
|
||||
child: SizedBox.expand(
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: selected ? 34 : 0,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(3),
|
||||
bottomRight: Radius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(destination.icon, size: 23, color: foregroundColor),
|
||||
const SizedBox(height: 5),
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
style: theme.textTheme.labelMedium!.copyWith(
|
||||
color: foregroundColor,
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
selected ? FontWeight.w800 : FontWeight.w600,
|
||||
letterSpacing: 0,
|
||||
height: 1.1,
|
||||
),
|
||||
child: Text(
|
||||
destination.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? _messageForError(AppLocalizations l10n, FocusSeedError? error) {
|
||||
return switch (error) {
|
||||
FocusSeedError.openFailed => l10n.openFailed,
|
||||
|
||||
@@ -1,56 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../application/focus_seed_controller.dart';
|
||||
import '../../application/focus_timer_controller.dart';
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../core/iterable_extensions.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../models/habit.dart';
|
||||
import '../../models/reflection_entry.dart';
|
||||
import '../../models/today_progress.dart';
|
||||
import '../../widgets/dialogs.dart';
|
||||
import '../../widgets/focus_seed_header.dart';
|
||||
import '../../widgets/surface_panel.dart';
|
||||
import '../../widgets/experience_motion.dart';
|
||||
import '../../widgets/workbench_ui.dart';
|
||||
import '../habits/habit_manager_sheet.dart';
|
||||
import '../reflection/reflection_sheet.dart';
|
||||
import 'widgets/daily_progress_rail.dart';
|
||||
import 'widgets/focus_session_card.dart';
|
||||
import 'widgets/habit_section.dart';
|
||||
import 'widgets/intention_card.dart';
|
||||
import 'widgets/reflection_prompt.dart';
|
||||
|
||||
class TodayScreen extends StatelessWidget {
|
||||
const TodayScreen({
|
||||
required this.controller,
|
||||
required this.timerController,
|
||||
required this.onOpenSettings,
|
||||
required this.onOpenFocus,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final FocusSeedController controller;
|
||||
final FocusTimerController timerController;
|
||||
final VoidCallback onOpenSettings;
|
||||
final VoidCallback onOpenFocus;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = controller.state!;
|
||||
final l10n = context.l10n;
|
||||
final now = DateTime.now();
|
||||
final today = todayKey();
|
||||
final progress = TodayProgress.fromState(state, today);
|
||||
final reflection =
|
||||
state.reflections.where((entry) => entry.dateKey == today).firstOrNull;
|
||||
final reflection = _reflectionForToday(state.reflections, today);
|
||||
final activeSession = timerController.isRunning || timerController.isPaused;
|
||||
|
||||
void editIntention() {
|
||||
showIntentionDialog(
|
||||
void startFocus() {
|
||||
unawaited(HapticFeedback.selectionClick());
|
||||
timerController.start();
|
||||
onOpenFocus();
|
||||
}
|
||||
|
||||
void openReflection() {
|
||||
showReflectionSheet(
|
||||
context,
|
||||
controller.setTodayAnchor,
|
||||
initialValue: progress.intention,
|
||||
controller,
|
||||
dateKey: today,
|
||||
contextTitle: progress.intention,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
FocusSeedHeader(
|
||||
title: localizedGreeting(l10n, now),
|
||||
subtitle: localizedReadableDate(context, now),
|
||||
isSaving: controller.isSaving,
|
||||
WorkbenchTopBar(
|
||||
title: l10n.brandName,
|
||||
trailingIcon: Icons.lock_outline_rounded,
|
||||
trailingTooltip: l10n.privacyTitle,
|
||||
onTrailingPressed: () => showPrivacySheet(context),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
@@ -62,37 +73,46 @@ class TodayScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
IntentionCard(
|
||||
intention: progress.intention,
|
||||
onEdit: editIntention,
|
||||
StaggeredReveal(
|
||||
child: _TodayCommandPanel(
|
||||
progress: progress,
|
||||
timerController: timerController,
|
||||
activeSession: activeSession,
|
||||
onEditFocus:
|
||||
() => showIntentionDialog(
|
||||
context,
|
||||
controller.setTodayAnchor,
|
||||
initialValue: progress.intention,
|
||||
),
|
||||
onStartFocus: startFocus,
|
||||
onOpenFocus: onOpenFocus,
|
||||
onReflect: openReflection,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FocusSessionCard(
|
||||
controller: timerController,
|
||||
growthStage: progress.growthStage,
|
||||
hasIntention: progress.hasIntention,
|
||||
onSetIntention: editIntention,
|
||||
onReflect: () => showReflectionSheet(context, controller),
|
||||
StaggeredReveal(
|
||||
order: 1,
|
||||
child: _HabitWorkbenchSection(
|
||||
habits: state.habits,
|
||||
dateKey: today,
|
||||
onToggle: (habit) => unawaited(controller.toggleHabit(habit)),
|
||||
onAdd: () => showHabitDialog(context, controller.addHabit),
|
||||
onManage: () => showHabitManagerSheet(context, controller),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SurfacePanel(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 10),
|
||||
child: DailyProgressRail(progress: progress),
|
||||
StaggeredReveal(
|
||||
order: 2,
|
||||
child: _ReflectionWorkbenchSection(
|
||||
reflection: reflection,
|
||||
onTap: openReflection,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
HabitSection(
|
||||
habits: state.habits,
|
||||
dateKey: today,
|
||||
onToggle: controller.toggleHabit,
|
||||
onAdd: () => showHabitDialog(context, controller.addHabit),
|
||||
onManage: () => showHabitManagerSheet(context, controller),
|
||||
StaggeredReveal(
|
||||
order: 3,
|
||||
child: _TodayLedgerStrip(progress: progress),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ReflectionPrompt(
|
||||
reflection: reflection,
|
||||
onTap: () => showReflectionSheet(context, controller),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -108,6 +128,568 @@ String _messageForError(AppLocalizations l10n, FocusSeedError error) {
|
||||
};
|
||||
}
|
||||
|
||||
ReflectionEntry? _reflectionForToday(
|
||||
List<ReflectionEntry> reflections,
|
||||
String today,
|
||||
) {
|
||||
final matches =
|
||||
reflections.where((entry) => entry.dateKey == today).toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return matches.isEmpty ? null : matches.first;
|
||||
}
|
||||
|
||||
class _TodayCommandPanel extends StatelessWidget {
|
||||
const _TodayCommandPanel({
|
||||
required this.progress,
|
||||
required this.timerController,
|
||||
required this.activeSession,
|
||||
required this.onEditFocus,
|
||||
required this.onStartFocus,
|
||||
required this.onOpenFocus,
|
||||
required this.onReflect,
|
||||
});
|
||||
|
||||
final TodayProgress progress;
|
||||
final FocusTimerController timerController;
|
||||
final bool activeSession;
|
||||
final VoidCallback onEditFocus;
|
||||
final VoidCallback onStartFocus;
|
||||
final VoidCallback onOpenFocus;
|
||||
final VoidCallback onReflect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final title =
|
||||
progress.hasIntention ? progress.intention : l10n.chooseOneOutcome;
|
||||
final status = _todayStatus(context);
|
||||
final primary = _primaryAction(context);
|
||||
return WorkbenchPanel(
|
||||
accent: visuals.action,
|
||||
fill: Color.alphaBlend(
|
||||
visuals.action.withValues(alpha: 0.035),
|
||||
scheme.surfaceContainerLowest,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 15, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
WorkbenchHeader(
|
||||
eyebrow: l10n.todayIntentionLabel,
|
||||
title: title,
|
||||
subtitle: status.$2,
|
||||
icon: activeSession ? Icons.timer_outlined : Icons.flag_outlined,
|
||||
accent: status.$3,
|
||||
trailing: IconButton(
|
||||
tooltip:
|
||||
progress.hasIntention
|
||||
? l10n.editIntentionTitle
|
||||
: l10n.setIntentionTitle,
|
||||
onPressed: onEditFocus,
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: const Size(40, 40),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(kWorkbenchRadius),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.edit_outlined, size: 20),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_TodayStepTrack(progress: progress, activeSession: activeSession),
|
||||
const SizedBox(height: 14),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeOutCubic,
|
||||
child: _TodayPrimaryAction(
|
||||
key: ValueKey(primary.$1),
|
||||
label: primary.$2,
|
||||
icon: primary.$3,
|
||||
accent: primary.$4,
|
||||
onPressed: primary.$5,
|
||||
),
|
||||
),
|
||||
if (!progress.hasIntention && !activeSession) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton.icon(
|
||||
onPressed: onEditFocus,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
label: Text(l10n.setTodaysIntention),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(String, String, Color) _todayStatus(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
if (timerController.isRunning) {
|
||||
return (
|
||||
'running',
|
||||
'${l10n.timerRunning} · ${formatSeconds(timerController.remainingSeconds)}',
|
||||
visuals.focus,
|
||||
);
|
||||
}
|
||||
if (timerController.isPaused) {
|
||||
return (
|
||||
'paused',
|
||||
'${l10n.timerPaused} · ${formatSeconds(timerController.remainingSeconds)}',
|
||||
visuals.habit,
|
||||
);
|
||||
}
|
||||
if (progress.isLoopClosed) {
|
||||
return ('closed', l10n.todayLoopDoneBody, visuals.success);
|
||||
}
|
||||
if (progress.hasFocus && !progress.hasReflection) {
|
||||
return ('reflect', l10n.nextActionReflectBody, visuals.reflection);
|
||||
}
|
||||
if (progress.hasIntention) {
|
||||
return ('ready', l10n.nextActionStartActionBody, visuals.focus);
|
||||
}
|
||||
return ('empty', l10n.todayLoopStartBody, visuals.action);
|
||||
}
|
||||
|
||||
(String, String, IconData, Color, VoidCallback) _primaryAction(
|
||||
BuildContext context,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
if (activeSession) {
|
||||
return (
|
||||
'continue',
|
||||
l10n.continueSession,
|
||||
Icons.arrow_forward_rounded,
|
||||
visuals.focus,
|
||||
onOpenFocus,
|
||||
);
|
||||
}
|
||||
if (progress.hasFocus && !progress.hasReflection) {
|
||||
return (
|
||||
'reflect',
|
||||
l10n.reflectionAction,
|
||||
Icons.edit_note_rounded,
|
||||
visuals.reflection,
|
||||
onReflect,
|
||||
);
|
||||
}
|
||||
return (
|
||||
'start',
|
||||
l10n.startMinutes(timerController.selectedMinutes),
|
||||
Icons.play_arrow_rounded,
|
||||
visuals.focus,
|
||||
onStartFocus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TodayPrimaryAction extends StatelessWidget {
|
||||
const _TodayPrimaryAction({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.accent,
|
||||
required this.onPressed,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color accent;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onPressed,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: accent,
|
||||
foregroundColor:
|
||||
ThemeData.estimateBrightnessForColor(accent) == Brightness.dark
|
||||
? Colors.white
|
||||
: scheme.onSurface,
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(kWorkbenchRadius),
|
||||
),
|
||||
),
|
||||
icon: Icon(icon),
|
||||
label: Text(label),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TodayStepTrack extends StatelessWidget {
|
||||
const _TodayStepTrack({required this.progress, required this.activeSession});
|
||||
|
||||
final TodayProgress progress;
|
||||
final bool activeSession;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final items = [
|
||||
_StepData(
|
||||
label: l10n.progressIntention,
|
||||
icon: Icons.flag_outlined,
|
||||
complete: progress.hasIntention,
|
||||
accent: visuals.action,
|
||||
),
|
||||
_StepData(
|
||||
label: l10n.progressFocus,
|
||||
icon: Icons.timer_outlined,
|
||||
complete: progress.hasFocus || activeSession,
|
||||
accent: visuals.focus,
|
||||
),
|
||||
_StepData(
|
||||
label: l10n.progressReflect,
|
||||
icon: Icons.edit_note_rounded,
|
||||
complete: progress.hasReflection,
|
||||
accent: visuals.reflection,
|
||||
),
|
||||
];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
WorkbenchProgressTrack(
|
||||
value: progress.completedSteps / 3,
|
||||
accent: progress.isLoopClosed ? visuals.success : visuals.focus,
|
||||
),
|
||||
const SizedBox(height: 9),
|
||||
Row(
|
||||
children: [
|
||||
for (var index = 0; index < items.length; index++) ...[
|
||||
Expanded(child: _StepMarker(data: items[index])),
|
||||
if (index != items.length - 1) const SizedBox(width: 8),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StepData {
|
||||
const _StepData({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.complete,
|
||||
required this.accent,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final bool complete;
|
||||
final Color accent;
|
||||
}
|
||||
|
||||
class _StepMarker extends StatelessWidget {
|
||||
const _StepMarker({required this.data});
|
||||
|
||||
final _StepData data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final color = data.complete ? data.accent : scheme.outline;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
data.complete
|
||||
? data.accent.withValues(alpha: 0.10)
|
||||
: scheme.surfaceContainerLow.withValues(alpha: 0.55),
|
||||
borderRadius: BorderRadius.circular(kWorkbenchRadius),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
data.complete ? Icons.check_rounded : data.icon,
|
||||
color: color,
|
||||
size: 15,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Expanded(
|
||||
child: Text(
|
||||
data.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color:
|
||||
data.complete ? scheme.onSurface : scheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HabitWorkbenchSection extends StatelessWidget {
|
||||
const _HabitWorkbenchSection({
|
||||
required this.habits,
|
||||
required this.dateKey,
|
||||
required this.onToggle,
|
||||
required this.onAdd,
|
||||
required this.onManage,
|
||||
});
|
||||
|
||||
final List<Habit> habits;
|
||||
final String dateKey;
|
||||
final ValueChanged<Habit> onToggle;
|
||||
final VoidCallback onAdd;
|
||||
final VoidCallback onManage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final completed =
|
||||
habits.where((habit) {
|
||||
return habit.completedDates.contains(dateKey);
|
||||
}).length;
|
||||
return WorkbenchPanel(
|
||||
accent: visuals.habit,
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
WorkbenchSectionTitle(
|
||||
title: l10n.todaysHabits,
|
||||
icon: Icons.add_task_rounded,
|
||||
accent: visuals.habit,
|
||||
trailing: WorkbenchPill(
|
||||
label: l10n.completedCount(completed, habits.length),
|
||||
accent: visuals.habit,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (habits.isEmpty)
|
||||
_QuietText(text: l10n.emptyHabitPrompt)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
for (var index = 0; index < habits.length; index++) ...[
|
||||
if (index > 0) const Divider(height: 1),
|
||||
_HabitWorkbenchRow(
|
||||
habit: habits[index],
|
||||
dateKey: dateKey,
|
||||
onToggle: () => onToggle(habits[index]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(l10n.addHabit),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
onPressed: onManage,
|
||||
icon: const Icon(Icons.tune_rounded),
|
||||
label: Text(l10n.manageHabits),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HabitWorkbenchRow extends StatelessWidget {
|
||||
const _HabitWorkbenchRow({
|
||||
required this.habit,
|
||||
required this.dateKey,
|
||||
required this.onToggle,
|
||||
});
|
||||
|
||||
final Habit habit;
|
||||
final String dateKey;
|
||||
final VoidCallback onToggle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final done = habit.completedDates.contains(dateKey);
|
||||
return WorkbenchListRow(
|
||||
title: habit.title,
|
||||
accent: visuals.habit,
|
||||
leadingIcon:
|
||||
done ? Icons.check_circle_rounded : Icons.radio_button_unchecked,
|
||||
trailing: Checkbox(
|
||||
value: done,
|
||||
activeColor: visuals.habit,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
onChanged: (_) => onToggle(),
|
||||
),
|
||||
onTap: onToggle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReflectionWorkbenchSection extends StatelessWidget {
|
||||
const _ReflectionWorkbenchSection({
|
||||
required this.reflection,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final ReflectionEntry? reflection;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final note = reflection?.note.trim() ?? '';
|
||||
final hasNote = note.isNotEmpty;
|
||||
return WorkbenchPanel(
|
||||
accent: visuals.reflection,
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
WorkbenchSectionTitle(
|
||||
title: l10n.progressReflect,
|
||||
icon: Icons.edit_note_rounded,
|
||||
accent: visuals.reflection,
|
||||
trailing: Icon(
|
||||
hasNote ? Icons.check_rounded : Icons.add_rounded,
|
||||
color: visuals.reflection,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
hasNote ? note : l10n.recordsNoReflection,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: hasNote ? FontWeight.w800 : FontWeight.w600,
|
||||
height: 1.28,
|
||||
),
|
||||
),
|
||||
if (!hasNote) ...[
|
||||
const SizedBox(height: 10),
|
||||
WorkbenchPill(
|
||||
label: l10n.reflectionAction,
|
||||
icon: Icons.arrow_forward_rounded,
|
||||
accent: visuals.reflection,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TodayLedgerStrip extends StatelessWidget {
|
||||
const _TodayLedgerStrip({required this.progress});
|
||||
|
||||
final TodayProgress progress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
return WorkbenchPanel(
|
||||
accent: visuals.privacy,
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
WorkbenchSectionTitle(
|
||||
title: l10n.todayLoopLabel,
|
||||
icon: Icons.inventory_2_outlined,
|
||||
accent: visuals.privacy,
|
||||
trailing: WorkbenchPill(
|
||||
label: l10n.local,
|
||||
icon: Icons.lock_outline_rounded,
|
||||
accent: visuals.privacy,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxWidth < 380;
|
||||
final children = [
|
||||
WorkbenchMetricCell(
|
||||
label: l10n.progressFocus,
|
||||
value: l10n.minutesShort(progress.focusMinutes),
|
||||
icon: Icons.timer_outlined,
|
||||
accent: visuals.focus,
|
||||
),
|
||||
WorkbenchMetricCell(
|
||||
label: l10n.progressHabits,
|
||||
value: l10n.completedCount(
|
||||
progress.completedHabits,
|
||||
progress.totalHabits,
|
||||
),
|
||||
icon: Icons.add_task_rounded,
|
||||
accent: visuals.habit,
|
||||
),
|
||||
WorkbenchMetricCell(
|
||||
label: l10n.progressReflect,
|
||||
value:
|
||||
progress.hasReflection
|
||||
? l10n.done
|
||||
: l10n.recordsNoReflection,
|
||||
icon: Icons.edit_note_rounded,
|
||||
accent: visuals.reflection,
|
||||
),
|
||||
];
|
||||
if (compact) {
|
||||
return Column(
|
||||
children: [
|
||||
for (var index = 0; index < children.length; index++) ...[
|
||||
if (index > 0) const SizedBox(height: 8),
|
||||
children[index],
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
for (var index = 0; index < children.length; index++) ...[
|
||||
Expanded(child: children[index]),
|
||||
if (index != children.length - 1) const SizedBox(width: 8),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InlineError extends StatelessWidget {
|
||||
const _InlineError({required this.message});
|
||||
|
||||
@@ -122,7 +704,7 @@ class _InlineError extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(kWorkbenchRadius),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -140,3 +722,21 @@ class _InlineError extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuietText extends StatelessWidget {
|
||||
const _QuietText({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
height: 1.28,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../../app/app_theme.dart';
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/habit.dart';
|
||||
import '../../../widgets/app_motion.dart';
|
||||
import '../../../widgets/experience_motion.dart';
|
||||
import '../../../widgets/pressable_scale.dart';
|
||||
|
||||
class CompactHabitRow extends StatelessWidget {
|
||||
const CompactHabitRow({
|
||||
@@ -28,75 +33,119 @@ class CompactHabitRow extends StatelessWidget {
|
||||
button: true,
|
||||
checked: complete,
|
||||
label: habit.title,
|
||||
child: InkWell(
|
||||
onTap: onToggle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
complete ? theme.colorScheme.primary : Colors.transparent,
|
||||
border: Border.all(
|
||||
color:
|
||||
complete
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outline,
|
||||
child: PressableScale(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
onToggle();
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: AppMotion.duration(context, AppMotion.fast),
|
||||
curve: AppMotion.emphasized,
|
||||
margin: const EdgeInsets.symmetric(vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
complete
|
||||
? FocusSeedVisuals.of(
|
||||
context,
|
||||
).success.withValues(alpha: 0.055)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_HabitCheckControl(complete: complete),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: AppMotion.duration(context, AppMotion.fast),
|
||||
style:
|
||||
theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight:
|
||||
complete ? FontWeight.w700 : FontWeight.w500,
|
||||
) ??
|
||||
const TextStyle(),
|
||||
child: Text(
|
||||
habit.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child: Icon(
|
||||
complete ? Icons.eco_rounded : Icons.circle_outlined,
|
||||
key: ValueKey(complete),
|
||||
size: 16,
|
||||
color:
|
||||
complete
|
||||
? theme.colorScheme.onPrimary
|
||||
: Colors.transparent,
|
||||
if (onEdit != null || onDelete != null)
|
||||
PopupMenuButton<String>(
|
||||
tooltip: l10n.habitOptions,
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
onEdit?.call();
|
||||
} else if (value == 'delete') {
|
||||
onDelete?.call();
|
||||
}
|
||||
},
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Text(l10n.rename),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text(l10n.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 11),
|
||||
Expanded(
|
||||
child: Text(
|
||||
habit.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color:
|
||||
complete
|
||||
? theme.colorScheme.onSurfaceVariant
|
||||
: theme.colorScheme.onSurface,
|
||||
decoration: complete ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onEdit != null || onDelete != null)
|
||||
PopupMenuButton<String>(
|
||||
tooltip: l10n.habitOptions,
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
onEdit?.call();
|
||||
} else if (value == 'delete') {
|
||||
onDelete?.call();
|
||||
}
|
||||
},
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(value: 'edit', child: Text(l10n.rename)),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text(l10n.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HabitCheckControl extends StatelessWidget {
|
||||
const _HabitCheckControl({required this.complete});
|
||||
|
||||
final bool complete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
return SizedBox.square(
|
||||
dimension: 32,
|
||||
child: Center(
|
||||
child: AnimatedScale(
|
||||
scale: complete ? 1.02 : 1,
|
||||
duration: AppMotion.duration(context, AppMotion.fast),
|
||||
curve: AppMotion.settle,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow:
|
||||
complete
|
||||
? [
|
||||
BoxShadow(
|
||||
color: visuals.success.withValues(alpha: 0.16),
|
||||
blurRadius: 9,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
color: scheme.shadow.withValues(alpha: 0.025),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: AnimatedCheckMark(
|
||||
checked: complete,
|
||||
color: visuals.success,
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../app/app_theme.dart';
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/today_progress.dart';
|
||||
import '../../../widgets/app_motion.dart';
|
||||
|
||||
class DailyProgressRail extends StatelessWidget {
|
||||
const DailyProgressRail({required this.progress, super.key});
|
||||
@@ -27,16 +29,10 @@ class DailyProgressRail extends StatelessWidget {
|
||||
icon: Icons.timer_outlined,
|
||||
complete: progress.hasFocus,
|
||||
),
|
||||
_ProgressItem(
|
||||
label: l10n.progressHabits,
|
||||
detail: '${progress.completedHabits}/${progress.totalHabits}',
|
||||
icon: Icons.eco_outlined,
|
||||
complete: progress.hasHabits,
|
||||
),
|
||||
_ProgressItem(
|
||||
label: l10n.progressReflect,
|
||||
detail: progress.hasReflection ? l10n.done : l10n.later,
|
||||
icon: Icons.auto_awesome_outlined,
|
||||
icon: Icons.edit_note_rounded,
|
||||
complete: progress.hasReflection,
|
||||
),
|
||||
];
|
||||
@@ -60,71 +56,94 @@ class _HorizontalProgress extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final lastCompleteIndex = items.lastIndexWhere((item) => item.complete);
|
||||
final progress =
|
||||
lastCompleteIndex <= 0 ? 0.0 : lastCompleteIndex / (items.length - 1);
|
||||
return Semantics(
|
||||
label: context.l10n.dailyStepsComplete(
|
||||
items.where((item) => item.complete).length,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < items.length; index++)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (index > 0)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 2,
|
||||
color:
|
||||
items[index - 1].complete
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outlineVariant,
|
||||
),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final inset = constraints.maxWidth / (items.length * 2);
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
left: inset,
|
||||
right: inset,
|
||||
top: 15,
|
||||
child: _ProgressTrack(
|
||||
color: scheme.outlineVariant.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: inset,
|
||||
right: inset,
|
||||
top: 15,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: progress.clamp(0.0, 1.0),
|
||||
child: _ProgressTrack(
|
||||
color: visuals.success,
|
||||
glow: visuals.success.withValues(alpha: 0.20),
|
||||
),
|
||||
if (index < items.length - 1)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 2,
|
||||
color:
|
||||
items[index].complete
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Row(
|
||||
children: [
|
||||
for (final item in items)
|
||||
Expanded(
|
||||
child: Center(child: _ProgressDot(item: item)),
|
||||
),
|
||||
),
|
||||
_ProgressDot(item: items[index]),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
items[index].label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
items[index].detail,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final item in items)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
item.detail,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
fontSize: 10.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -161,6 +180,28 @@ class _VerticalProgress extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressTrack extends StatelessWidget {
|
||||
const _ProgressTrack({required this.color, this.glow});
|
||||
|
||||
final Color color;
|
||||
final Color? glow;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: AppMotion.duration(context, AppMotion.medium),
|
||||
curve: AppMotion.emphasized,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
boxShadow:
|
||||
glow == null ? null : [BoxShadow(color: glow!, blurRadius: 10)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressDot extends StatelessWidget {
|
||||
const _ProgressDot({required this.item});
|
||||
|
||||
@@ -169,29 +210,51 @@ class _ProgressDot extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
item.complete
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.surfaceContainerLowest,
|
||||
border: Border.all(
|
||||
color:
|
||||
final scheme = theme.colorScheme;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
return AnimatedScale(
|
||||
scale: item.complete ? 1.08 : 1,
|
||||
duration: AppMotion.duration(context, AppMotion.fast),
|
||||
curve: AppMotion.settle,
|
||||
child: AnimatedContainer(
|
||||
duration: AppMotion.duration(context, AppMotion.medium),
|
||||
curve: AppMotion.emphasized,
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: item.complete ? visuals.success : scheme.surfaceContainerLow,
|
||||
border: Border.all(
|
||||
color: item.complete ? visuals.success : scheme.outlineVariant,
|
||||
),
|
||||
boxShadow:
|
||||
item.complete
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outline,
|
||||
? [
|
||||
BoxShadow(
|
||||
color: visuals.success.withValues(alpha: 0.20),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: AppMotion.duration(context, AppMotion.fast),
|
||||
switchInCurve: AppMotion.settle,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(scale: animation, child: child),
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
item.complete ? Icons.check_rounded : item.icon,
|
||||
key: ValueKey('${item.label}-${item.complete}'),
|
||||
size: 15,
|
||||
color: item.complete ? scheme.onPrimary : scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
item.complete ? Icons.check_rounded : item.icon,
|
||||
size: 17,
|
||||
color:
|
||||
item.complete
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../../application/focus_timer_controller.dart';
|
||||
import '../../../core/date_keys.dart';
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/growth_stage.dart';
|
||||
import '../../../widgets/growth_object.dart';
|
||||
import '../../../widgets/surface_panel.dart';
|
||||
|
||||
class FocusSessionCard extends StatelessWidget {
|
||||
const FocusSessionCard({
|
||||
required this.controller,
|
||||
required this.growthStage,
|
||||
required this.hasIntention,
|
||||
required this.onSetIntention,
|
||||
required this.onReflect,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final FocusTimerController controller;
|
||||
final GrowthStage growthStage;
|
||||
final bool hasIntention;
|
||||
final VoidCallback onSetIntention;
|
||||
final VoidCallback onReflect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
return SurfacePanel(
|
||||
elevated: true,
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.focusLabel,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textScale =
|
||||
MediaQuery.textScalerOf(context).scale(16) / 16;
|
||||
final stack = constraints.maxWidth < 260 || textScale > 1.6;
|
||||
final details = _TimerDetails(controller: controller);
|
||||
final plant = GrowthObject(
|
||||
stage: growthStage,
|
||||
size: stack ? 116 : 122,
|
||||
);
|
||||
if (stack) {
|
||||
return Column(
|
||||
children: [plant, const SizedBox(height: 4), details],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: details),
|
||||
const SizedBox(width: 8),
|
||||
plant,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
if (!controller.isRunning && !controller.isCompleted)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: SegmentedButton<int>(
|
||||
segments: [
|
||||
for (final minutes in FocusTimerController.presets)
|
||||
ButtonSegment(value: minutes, label: Text('$minutes')),
|
||||
],
|
||||
selected: {controller.selectedMinutes},
|
||||
onSelectionChanged: (selection) {
|
||||
HapticFeedback.selectionClick();
|
||||
controller.selectMinutes(selection.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (controller.isRunning || controller.isPaused) ...[
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: controller.progress,
|
||||
minHeight: 7,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
_TimerActions(
|
||||
controller: controller,
|
||||
hasIntention: hasIntention,
|
||||
onSetIntention: onSetIntention,
|
||||
onReflect: onReflect,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimerDetails extends StatelessWidget {
|
||||
const _TimerDetails({required this.controller});
|
||||
|
||||
final FocusTimerController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = context.l10n;
|
||||
final status = switch (controller.status) {
|
||||
FocusTimerStatus.idle => l10n.timerReady,
|
||||
FocusTimerStatus.running => l10n.timerRunning,
|
||||
FocusTimerStatus.paused => l10n.timerPaused,
|
||||
FocusTimerStatus.completed => l10n.timerCompleted,
|
||||
};
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child: Text(
|
||||
formatSeconds(controller.remainingSeconds),
|
||||
key: ValueKey(controller.remainingSeconds),
|
||||
style: theme.textTheme.displaySmall?.copyWith(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
controller.isCompleted
|
||||
? theme.colorScheme.tertiary
|
||||
: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 7),
|
||||
Flexible(
|
||||
child: Text(
|
||||
status,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimerActions extends StatelessWidget {
|
||||
const _TimerActions({
|
||||
required this.controller,
|
||||
required this.hasIntention,
|
||||
required this.onSetIntention,
|
||||
required this.onReflect,
|
||||
});
|
||||
|
||||
final FocusTimerController controller;
|
||||
final bool hasIntention;
|
||||
final VoidCallback onSetIntention;
|
||||
final VoidCallback onReflect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
if (controller.isCompleted) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: onReflect,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
label: Text(l10n.reflectOneMinute),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.outlined(
|
||||
tooltip: l10n.startAnotherSession,
|
||||
onPressed: controller.reset,
|
||||
icon: const Icon(Icons.restart_alt_rounded),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (!hasIntention && !controller.isRunning && !controller.isPaused) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onSetIntention,
|
||||
icon: const Icon(Icons.flag_outlined),
|
||||
label: Text(l10n.setTodaysIntention),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (controller.isRunning) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: controller.pause,
|
||||
icon: const Icon(Icons.pause_rounded),
|
||||
label: Text(l10n.pause),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: controller.completeNow,
|
||||
icon: const Icon(Icons.stop_rounded),
|
||||
label: Text(l10n.end),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
controller.start();
|
||||
},
|
||||
icon: Icon(
|
||||
controller.isPaused
|
||||
? Icons.play_arrow_rounded
|
||||
: Icons.play_circle_outline_rounded,
|
||||
),
|
||||
label: Text(
|
||||
controller.isPaused
|
||||
? l10n.resume
|
||||
: l10n.startMinutes(controller.selectedMinutes),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.isPaused) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton.outlined(
|
||||
tooltip: l10n.resetTimer,
|
||||
onPressed: controller.reset,
|
||||
icon: const Icon(Icons.restart_alt_rounded),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../app/app_theme.dart';
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/habit.dart';
|
||||
import '../../../widgets/surface_panel.dart';
|
||||
@@ -24,10 +25,16 @@ class HabitSection extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final completed =
|
||||
habits.where((habit) => habit.completedDates.contains(dateKey)).length;
|
||||
final completion =
|
||||
habits.isEmpty ? 0.0 : (completed / habits.length).clamp(0.0, 1.0);
|
||||
return SurfacePanel(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 12, 8),
|
||||
tone: SurfacePanelTone.paper,
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 14, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -38,12 +45,14 @@ class HabitSection extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
l10n.todaysHabits,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
l10n.completedCount(completed, habits.length),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -61,16 +70,15 @@ class HabitSection extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 12),
|
||||
_HabitCompletionTrack(progress: completion),
|
||||
const SizedBox(height: 6),
|
||||
if (habits.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 16, 4, 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.eco_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Icon(Icons.eco_outlined, color: visuals.habit),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(l10n.emptyHabitPrompt)),
|
||||
TextButton(onPressed: onAdd, child: Text(l10n.add)),
|
||||
@@ -79,7 +87,7 @@ class HabitSection extends StatelessWidget {
|
||||
)
|
||||
else
|
||||
for (var index = 0; index < habits.take(4).length; index++) ...[
|
||||
if (index > 0) const Divider(),
|
||||
if (index > 0) const Divider(indent: 48),
|
||||
CompactHabitRow(
|
||||
habit: habits[index],
|
||||
dateKey: dateKey,
|
||||
@@ -87,7 +95,7 @@ class HabitSection extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
if (habits.length > 4) ...[
|
||||
const Divider(),
|
||||
const Divider(indent: 48),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
@@ -102,3 +110,48 @@ class HabitSection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HabitCompletionTrack extends StatelessWidget {
|
||||
const _HabitCompletionTrack({required this.progress});
|
||||
|
||||
final double progress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
child: SizedBox(
|
||||
height: 5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ColoredBox(color: scheme.outlineVariant.withValues(alpha: 0.46)),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(end: progress),
|
||||
duration: const Duration(milliseconds: 260),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, child) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: value,
|
||||
heightFactor: 1,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: visuals.success,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,198 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../app/app_theme.dart';
|
||||
import '../../../l10n/l10n.dart';
|
||||
import '../../../models/today_progress.dart';
|
||||
import '../../../widgets/surface_panel.dart';
|
||||
|
||||
class IntentionCard extends StatelessWidget {
|
||||
const IntentionCard({
|
||||
required this.intention,
|
||||
required this.progress,
|
||||
required this.onEdit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String intention;
|
||||
final TodayProgress progress;
|
||||
final VoidCallback onEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final l10n = context.l10n;
|
||||
final isEmpty = intention.isEmpty;
|
||||
final body =
|
||||
isEmpty
|
||||
? l10n.todayLoopStartBody
|
||||
: progress.completedSteps >= 3
|
||||
? l10n.todayLoopDoneBody
|
||||
: l10n.todayLoopActiveBody;
|
||||
return SurfacePanel(
|
||||
elevated: !isEmpty,
|
||||
tone: SurfacePanelTone.paper,
|
||||
onTap: onEdit,
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 15),
|
||||
color: Color.alphaBlend(
|
||||
visuals.action.withValues(alpha: isEmpty ? 0.035 : 0.025),
|
||||
theme.colorScheme.surfaceContainerLowest,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withValues(alpha: 0.11),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(Icons.flag_outlined, color: theme.colorScheme.primary),
|
||||
),
|
||||
const SizedBox(width: 13),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.todayIntentionLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 5,
|
||||
height: 58,
|
||||
decoration: BoxDecoration(
|
||||
color: visuals.action,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.colorScheme.primary.withValues(alpha: 0.24),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
isEmpty ? l10n.chooseOneOutcome : intention,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontSize: 18,
|
||||
color:
|
||||
isEmpty
|
||||
? theme.colorScheme.onSurfaceVariant
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.todayIntentionLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: visuals.action,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
isEmpty ? l10n.chooseOneOutcome : intention,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.08,
|
||||
color:
|
||||
isEmpty
|
||||
? theme.colorScheme.onSurfaceVariant
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
body,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (isEmpty) ...[
|
||||
const SizedBox(height: 14),
|
||||
FilledButton.icon(
|
||||
onPressed: onEdit,
|
||||
icon: const Icon(Icons.flag_outlined),
|
||||
label: Text(l10n.setTodaysIntention),
|
||||
),
|
||||
],
|
||||
if (!isEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_StatusChip(
|
||||
icon: Icons.check_circle_outline_rounded,
|
||||
label: l10n.todayLoopProgress(progress.completedSteps),
|
||||
color: visuals.success,
|
||||
),
|
||||
_StatusChip(
|
||||
icon: Icons.timer_outlined,
|
||||
label: l10n.minutesShort(
|
||||
progress.focusMinutes == 0 ? 10 : progress.focusMinutes,
|
||||
),
|
||||
color: visuals.focus,
|
||||
),
|
||||
_StatusChip(
|
||||
icon: Icons.edit_note_rounded,
|
||||
label:
|
||||
progress.hasReflection
|
||||
? l10n.reflectionCaptured
|
||||
: l10n.reflectionAction,
|
||||
color: visuals.reflection,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
isEmpty ? Icons.add_rounded : Icons.edit_outlined,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
size: 21,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusChip extends StatelessWidget {
|
||||
const _StatusChip({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.08),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 178),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ReflectionPrompt extends StatelessWidget {
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final isComplete = reflection != null;
|
||||
return SurfacePanel(
|
||||
borderColor: visuals.reflection.withValues(alpha: 0.35),
|
||||
tone: SurfacePanelTone.paper,
|
||||
color: visuals.reflection.withValues(alpha: 0.06),
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
|
||||
+125
-57
@@ -5,11 +5,12 @@
|
||||
"saving": "Saving",
|
||||
"local": "Local",
|
||||
"navToday": "Today",
|
||||
"navJourney": "Journey",
|
||||
"navRecords": "Records",
|
||||
"navMe": "Me",
|
||||
"tryAgain": "Try again",
|
||||
"couldNotOpenApp": "Could not open Focus Seed.",
|
||||
"openFailed": "Your local garden could not be opened.",
|
||||
"loadingLocalData": "Loading local data",
|
||||
"openFailed": "Your local data could not be opened.",
|
||||
"saveFailed": "That change could not be saved. Please try again.",
|
||||
"goodMorning": "Good morning",
|
||||
"goodDay": "Good day",
|
||||
@@ -33,11 +34,23 @@
|
||||
"timerRunning": "Deep work in progress",
|
||||
"timerPaused": "Paused - your place is kept",
|
||||
"timerCompleted": "Session complete",
|
||||
"reflectOneMinute": "Reflect for 1 min",
|
||||
"focusCurrentIntention": "Today's focus: {intention}",
|
||||
"@focusCurrentIntention": {
|
||||
"placeholders": {
|
||||
"intention": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"focusNoIntentionTitle": "Start this session first",
|
||||
"reflectOneMinute": "Write progress note",
|
||||
"startFocus": "Start focus",
|
||||
"startAnotherSession": "Start another session",
|
||||
"setTodaysIntention": "Set today's focus",
|
||||
"pause": "Pause",
|
||||
"end": "End",
|
||||
"completeSession": "Complete session",
|
||||
"continueSession": "Continue session",
|
||||
"resume": "Resume",
|
||||
"startMinutes": "Start {minutes} min",
|
||||
"@startMinutes": {
|
||||
@@ -63,7 +76,7 @@
|
||||
"manageHabits": "Manage habits",
|
||||
"addHabit": "Add habit",
|
||||
"add": "Add",
|
||||
"emptyHabitPrompt": "Add one repeatable habit.",
|
||||
"emptyHabitPrompt": "Keep this optional and small.",
|
||||
"viewAllHabits": "View all {count}",
|
||||
"@viewAllHabits": {
|
||||
"placeholders": {
|
||||
@@ -90,7 +103,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dailyStepsComplete": "{completed}/4 daily steps complete",
|
||||
"dailyStepsComplete": "{completed}/3 daily steps complete",
|
||||
"@dailyStepsComplete": {
|
||||
"placeholders": {
|
||||
"completed": {
|
||||
@@ -101,16 +114,17 @@
|
||||
"habitOptions": "Habit options",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"habitsTitle": "Habits",
|
||||
"habitsSubtitle": "Keep it small and repeatable.",
|
||||
"emptyHabitsTitle": "Add a small habit",
|
||||
"habitsTitle": "Supporting habits",
|
||||
"habitsSubtitle": "Optional repeatable steps. Today's focus still comes first.",
|
||||
"emptyHabitsTitle": "Add a supporting habit",
|
||||
"addHabitTitle": "Add habit",
|
||||
"renameHabitTitle": "Rename habit",
|
||||
"habitHint": "Example: Read 10 pages",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"setIntentionTitle": "Set intention",
|
||||
"editIntentionTitle": "Edit intention",
|
||||
"textEntryRequired": "Write a short line first.",
|
||||
"setIntentionTitle": "Set today's focus",
|
||||
"editIntentionTitle": "Edit today's focus",
|
||||
"intentionHint": "What matters most today?",
|
||||
"deleteHabitTitle": "Delete habit?",
|
||||
"deleteHabitBody": "Past completion records for this habit will be removed.",
|
||||
@@ -124,20 +138,22 @@
|
||||
"deleteAll": "Delete all",
|
||||
"privacyTitle": "Privacy & data",
|
||||
"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 clear today's progress or delete all local data from Me at any time.",
|
||||
"privacyBodyTwo": "You can clear today's progress or delete all local data from settings at any time.",
|
||||
"reflectionEmptyError": "Write one honest sentence.",
|
||||
"reflectionSaveError": "Could not save. Please try again.",
|
||||
"reflectionSheetTitle": "How did it go?",
|
||||
"reflectionSheetSubtitle": "Keep what worked. Adjust one thing.",
|
||||
"reflectionHint": "One useful sentence is enough.",
|
||||
"reflectionSheetTitle": "Leave one progress note",
|
||||
"reflectionSheetSubtitle": "Save what moved forward so next time has a clear start.",
|
||||
"reflectionHint": "What moved forward?",
|
||||
"reflectionMoodLabel": "Mood is optional",
|
||||
"savingLabel": "Saving",
|
||||
"saveReflection": "Save reflection",
|
||||
"saveReflection": "Save note",
|
||||
"moodCalm": "Calm",
|
||||
"moodClear": "Clear",
|
||||
"moodProud": "Proud",
|
||||
"moodTired": "Tired",
|
||||
"meTitle": "Your garden",
|
||||
"meSubtitle": "Preferences and records stay on this device.",
|
||||
"meTitle": "Me",
|
||||
"meSubtitle": "Manage your rhythm, appearance, and records.",
|
||||
"appSettingsTitle": "App settings",
|
||||
"appearance": "Theme",
|
||||
"system": "System",
|
||||
"light": "Light",
|
||||
@@ -148,14 +164,19 @@
|
||||
"languageEnglish": "English",
|
||||
"languageChinese": "Chinese",
|
||||
"preferencesTitle": "Preferences",
|
||||
"preferencesDetail": "Make it feel right",
|
||||
"preferencesDetail": "Theme, language, and default focus",
|
||||
"defaultFocusDuration": "Default focus",
|
||||
"defaultFocusDurationDetail": "Next session",
|
||||
"localData": "Local records",
|
||||
"localDataDetail": "Private on device",
|
||||
"personalGardenTitle": "Your focus garden",
|
||||
"personalGardenSubtitle": "Every small session leaves a trace you can return to.",
|
||||
"personalGardenTitle": "My accumulation",
|
||||
"personalGardenSubtitle": "Your local action records, progress notes, and app controls stay on this device.",
|
||||
"focusDataLabel": "Focus",
|
||||
"habitsDataLabel": "Habits",
|
||||
"notesDataLabel": "Notes",
|
||||
"notesDataLabel": "Progress",
|
||||
"totalActionTimeLabel": "Action time",
|
||||
"recordsCountLabel": "Records",
|
||||
"progressNotesCountLabel": "Progress notes",
|
||||
"clearToday": "Clear today",
|
||||
"clearTodaySubtitle": "Reset only today's loop.",
|
||||
"privacySubtitle": "No account or cloud sync.",
|
||||
@@ -163,24 +184,22 @@
|
||||
"deleteAllLocalDataSubtitle": "Remove this device's Focus Seed records.",
|
||||
"todayDataCleared": "Today's data cleared",
|
||||
"localDataCleared": "Local data deleted",
|
||||
"journeyTitle": "Progress",
|
||||
"journeyOverviewLabel": "TODAY'S LOOP",
|
||||
"journeyOverviewStartTitle": "Start with one clear focus",
|
||||
"journeyOverviewStartBody": "Pick the thing that would make today feel finished.",
|
||||
"journeyOverviewActiveTitle": "Your day is taking shape",
|
||||
"journeyOverviewActiveBody": "Keep the loop moving: focus, habits, then a short reflection.",
|
||||
"journeyOverviewDoneTitle": "Loop complete",
|
||||
"journeyOverviewDoneBody": "You left a clean record for future you.",
|
||||
"journeyOverviewProgress": "{completed}/4 done",
|
||||
"@journeyOverviewProgress": {
|
||||
"todayLoopLabel": "TODAY'S LOOP",
|
||||
"todayLoopStartTitle": "Start with one clear focus",
|
||||
"todayLoopStartBody": "Pick the one result that matters today.",
|
||||
"todayLoopActiveTitle": "Your day is taking shape",
|
||||
"todayLoopActiveBody": "Focus once, then leave one useful note.",
|
||||
"todayLoopDoneTitle": "Progress saved",
|
||||
"todayLoopDoneBody": "You moved today forward and left a clear place to continue.",
|
||||
"todayLoopProgress": "{completed}/3 done",
|
||||
"@todayLoopProgress": {
|
||||
"placeholders": {
|
||||
"completed": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reviewNow": "Reflect now",
|
||||
"journeyStatsTitle": "7-day rhythm",
|
||||
"reviewNow": "Write a note",
|
||||
"weekFocusLabel": "Focus",
|
||||
"streakLabel": "Streak",
|
||||
"activeDaysLabel": "Active",
|
||||
@@ -207,9 +226,28 @@
|
||||
"focusHistory": "Focus",
|
||||
"notesHistory": "Notes",
|
||||
"emptyFocusHistoryTitle": "No focus sessions yet",
|
||||
"emptyFocusHistoryBody": "Start one session from Today and it will appear here.",
|
||||
"emptyFocusHistoryBody": "Finish one focus session and it will appear here.",
|
||||
"emptyNotesHistoryTitle": "No notes yet",
|
||||
"emptyNotesHistoryBody": "Save a short reflection after focus to make your progress visible.",
|
||||
"emptyNotesHistoryBody": "Save one reflection after a session and it will appear here.",
|
||||
"recordsEmptyTitle": "No progress records yet",
|
||||
"recordsEmptyBody": "Finish one focus session and leave a note to keep today's progress here.",
|
||||
"recordsCalendarTitle": "Activity dates",
|
||||
"recordsDateLedgerTitle": "Date ledger",
|
||||
"backToToday": "Back to Today",
|
||||
"recordsNoIntention": "Unnamed daily focus",
|
||||
"recordsNoReflection": "No progress note yet",
|
||||
"recordsMissingProgressHelper": "Add one line so this is easier to pick up later.",
|
||||
"addProgressNote": "Add progress note",
|
||||
"continueThisAction": "Continue this",
|
||||
"replaceTodayActionTitle": "Replace today's action?",
|
||||
"replaceTodayActionBody": "This will change today's action to \"{actionTitle}\". Existing records will stay.",
|
||||
"@replaceTodayActionBody": {
|
||||
"placeholders": {
|
||||
"actionTitle": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recentFocus": "Recent focus",
|
||||
"sessionsTotal": "{count} sessions total",
|
||||
"@sessionsTotal": {
|
||||
@@ -250,32 +288,62 @@
|
||||
}
|
||||
},
|
||||
"reflectionCaptured": "Today's lesson is captured",
|
||||
"reflectionAction": "Close the loop with one sentence",
|
||||
"growthTitle": "Growth",
|
||||
"growthFlowerOpen": "Today's flower is open",
|
||||
"growthLocked": "Unlocked after reflection",
|
||||
"sevenDayGrowth": "7-day growth",
|
||||
"sevenDayGrowthSubtitle": "Small actions become visible over time.",
|
||||
"growthSeed": "seed",
|
||||
"growthSprout": "sprout",
|
||||
"growthLeaves": "leaves",
|
||||
"growthFlower": "flower",
|
||||
"growthDaySemantic": "{date}, {stage}",
|
||||
"@growthDaySemantic": {
|
||||
"reflectionAction": "Write one sentence to close today",
|
||||
"nextActionTitle": "Next action",
|
||||
"nextActionSetFocusTitle": "Write today's focus",
|
||||
"nextActionSetFocusBody": "One short line is enough. You can adjust it later.",
|
||||
"nextActionStartFocusTitle": "Start 10 minutes",
|
||||
"nextActionStartFocusBody": "Writing the action is optional. Start first.",
|
||||
"nextActionStartActionBody": "Move this forward in one small session, then leave one progress note.",
|
||||
"nextActionHabitsTitle": "Finish one small habit",
|
||||
"nextActionHabitsBody": "Complete one repeatable step while it is still easy.",
|
||||
"nextActionReflectTitle": "Write a quick note",
|
||||
"nextActionReflectBody": "Record what moved forward so the next start is easier.",
|
||||
"nextActionCompleteTitle": "Today moved forward",
|
||||
"nextActionCompleteBody": "Your note is saved. Continue only if it still feels useful.",
|
||||
"nextActionContinueFocus": "Focus {minutes} min again",
|
||||
"@nextActionContinueFocus": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "Object"
|
||||
},
|
||||
"stage": {
|
||||
"type": "Object"
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"growthSeedSemantic": "A seed waiting for today's intention",
|
||||
"growthSproutSemantic": "A sprout growing from today's intention",
|
||||
"growthLeavesSemantic": "A young plant growing with today's progress",
|
||||
"growthFlowerSemantic": "A flower marking a complete daily cycle",
|
||||
"habitTemplatesTitle": "Useful starts",
|
||||
"habitTemplatesBody": "Add one tiny repeatable action.",
|
||||
"habitTemplateStretch": "2 min stretch",
|
||||
"habitTemplateRead": "Read 10 pages",
|
||||
"habitTemplateWalk": "Walk outside",
|
||||
"habitTemplateNoScroll": "No-scroll lunch",
|
||||
"languageDetail": "Interface language",
|
||||
"recordsAndPrivacyTitle": "Records & privacy",
|
||||
"recordsAndPrivacyBody": "Your history stays local. Keep, reset, or delete it when needed.",
|
||||
"welcomeTrustLabel": "TODAY",
|
||||
"welcomeTitle": "Plant just one thing each day.",
|
||||
"welcomeBody": "Start with today's most important thing.",
|
||||
"welcomeNext": "The next step starts here.",
|
||||
"onboardingFocusBadge": "FOCUS",
|
||||
"onboardingFocusTitle": "Protect 10 minutes first.",
|
||||
"onboardingFocusBody": "It does not have to be long. Starting creates progress.",
|
||||
"onboardingFocusNext": "Keep this one thing in front of you while you focus.",
|
||||
"onboardingReflectBadge": "NOTE",
|
||||
"onboardingReflectTitle": "Leave one progress note.",
|
||||
"onboardingReflectBody": "When you finish, write one line so you know where to continue next time.",
|
||||
"onboardingReflectNext": "Now set today's focus.",
|
||||
"onboardingNext": "Continue",
|
||||
"welcomeStart": "Set today's focus",
|
||||
"welcomeSkip": "Skip",
|
||||
"landingTitle": "Start with 10 minutes.",
|
||||
"landingBody": "Writing is optional. Start first.",
|
||||
"landingFocusLabel": "What do you want to do first?",
|
||||
"landingFocusEntry": "Write down what you will do",
|
||||
"landingFocusHint": "Example: finish the launch checklist",
|
||||
"customFocusDuration": "Custom length",
|
||||
"customFocusDurationHint": "Enter 1-180 minutes",
|
||||
"minutesInputLabel": "Minutes",
|
||||
"landingStart": "Start 10 minutes",
|
||||
"landingSkip": "Enter Today",
|
||||
"defaultHabitPlanTask": "Plan one clear task",
|
||||
"defaultHabitFocus": "Focus for 25 minutes",
|
||||
"defaultHabitFocus": "Focus for 10 minutes",
|
||||
"defaultHabitReflect": "Write one-line reflection"
|
||||
}
|
||||
|
||||
+238
-170
@@ -2,19 +2,20 @@
|
||||
"@@locale": "zh",
|
||||
"appTitle": "Focus Seed",
|
||||
"brandName": "Focus Seed",
|
||||
"saving": "\u4fdd\u5b58\u4e2d",
|
||||
"local": "\u672c\u5730",
|
||||
"navToday": "\u4eca\u65e5",
|
||||
"navJourney": "\u8fdb\u5c55",
|
||||
"navMe": "\u6211\u7684",
|
||||
"tryAgain": "\u91cd\u8bd5",
|
||||
"couldNotOpenApp": "\u65e0\u6cd5\u6253\u5f00 Focus Seed\u3002",
|
||||
"openFailed": "\u672c\u5730\u6570\u636e\u6253\u5f00\u5931\u8d25\u3002",
|
||||
"saveFailed": "\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002",
|
||||
"goodMorning": "\u65e9\u4e0a\u597d",
|
||||
"goodDay": "\u4eca\u5929\u597d",
|
||||
"goodEvening": "\u665a\u4e0a\u597d",
|
||||
"goodNight": "\u591c\u6df1\u4e86",
|
||||
"saving": "保存中",
|
||||
"local": "本地",
|
||||
"navToday": "今日",
|
||||
"navRecords": "记录",
|
||||
"navMe": "我的",
|
||||
"tryAgain": "重试",
|
||||
"couldNotOpenApp": "无法打开 Focus Seed。",
|
||||
"loadingLocalData": "正在读取本地数据",
|
||||
"openFailed": "本地数据打开失败。",
|
||||
"saveFailed": "保存失败,请重试。",
|
||||
"goodMorning": "早上好",
|
||||
"goodDay": "今天好",
|
||||
"goodEvening": "晚上好",
|
||||
"goodNight": "夜深了",
|
||||
"readableDate": "{date} - {weekday}",
|
||||
"@readableDate": {
|
||||
"placeholders": {
|
||||
@@ -26,20 +27,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"todayIntentionLabel": "\u4eca\u65e5\u91cd\u70b9",
|
||||
"chooseOneOutcome": "\u5199\u4e0b\u4eca\u5929\u7684\u91cd\u70b9",
|
||||
"focusLabel": "\u4e13\u6ce8",
|
||||
"timerReady": "\u51c6\u5907\u597d\u4e86\u5c31\u5f00\u59cb",
|
||||
"timerRunning": "\u4e13\u6ce8\u4e2d",
|
||||
"timerPaused": "\u5df2\u6682\u505c",
|
||||
"timerCompleted": "\u5df2\u5b8c\u6210",
|
||||
"reflectOneMinute": "\u590d\u76d8 1 \u5206\u949f",
|
||||
"startAnotherSession": "\u518d\u6765\u4e00\u8f6e",
|
||||
"setTodaysIntention": "\u8bbe\u7f6e\u4eca\u65e5\u91cd\u70b9",
|
||||
"pause": "\u6682\u505c",
|
||||
"end": "\u7ed3\u675f",
|
||||
"resume": "\u7ee7\u7eed",
|
||||
"startMinutes": "\u5f00\u59cb {minutes} \u5206\u949f",
|
||||
"todayIntentionLabel": "今日重点",
|
||||
"chooseOneOutcome": "写下今天的重点",
|
||||
"focusLabel": "专注",
|
||||
"timerReady": "准备好了就开始",
|
||||
"timerRunning": "专注中",
|
||||
"timerPaused": "已暂停",
|
||||
"timerCompleted": "已完成",
|
||||
"focusCurrentIntention": "今日重点:{intention}",
|
||||
"@focusCurrentIntention": {
|
||||
"placeholders": {
|
||||
"intention": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"focusNoIntentionTitle": "先开始这一段",
|
||||
"reflectOneMinute": "写一句进展",
|
||||
"startFocus": "开始专注",
|
||||
"startAnotherSession": "再来一轮",
|
||||
"setTodaysIntention": "设置今日重点",
|
||||
"pause": "暂停",
|
||||
"end": "结束",
|
||||
"completeSession": "完成本轮",
|
||||
"continueSession": "继续本轮",
|
||||
"resume": "继续",
|
||||
"startMinutes": "开始 {minutes} 分钟",
|
||||
"@startMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -47,9 +60,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"resetTimer": "\u91cd\u7f6e\u8ba1\u65f6",
|
||||
"todaysHabits": "\u4eca\u65e5\u4e60\u60ef",
|
||||
"completedCount": "{completed}/{total} \u5b8c\u6210",
|
||||
"resetTimer": "重置计时",
|
||||
"todaysHabits": "今日习惯",
|
||||
"completedCount": "{completed}/{total} 完成",
|
||||
"@completedCount": {
|
||||
"placeholders": {
|
||||
"completed": {
|
||||
@@ -60,11 +73,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manageHabits": "\u7ba1\u7406\u4e60\u60ef",
|
||||
"addHabit": "\u6dfb\u52a0\u4e60\u60ef",
|
||||
"add": "\u6dfb\u52a0",
|
||||
"emptyHabitPrompt": "\u6dfb\u52a0\u4e00\u4e2a\u53ef\u91cd\u590d\u7684\u5c0f\u4e60\u60ef\u3002",
|
||||
"viewAllHabits": "\u67e5\u770b\u5168\u90e8 {count} \u4e2a",
|
||||
"manageHabits": "管理习惯",
|
||||
"addHabit": "添加习惯",
|
||||
"add": "添加",
|
||||
"emptyHabitPrompt": "保持可选,也保持很小。",
|
||||
"viewAllHabits": "查看全部 {count} 个",
|
||||
"@viewAllHabits": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -72,17 +85,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"reflectionSaved": "\u590d\u76d8\u5df2\u4fdd\u5b58",
|
||||
"reflectionPromptTitle": "\u590d\u76d8\u4e00\u4e0b",
|
||||
"reflectionPromptBody": "\u5199\u4e00\u53e5\u6709\u7528\u7684\u603b\u7ed3\u3002",
|
||||
"progressIntention": "\u76ee\u6807",
|
||||
"progressFocus": "\u4e13\u6ce8",
|
||||
"progressHabits": "\u4e60\u60ef",
|
||||
"progressReflect": "\u590d\u76d8",
|
||||
"done": "\u5b8c\u6210",
|
||||
"next": "\u4e0b\u4e00\u6b65",
|
||||
"later": "\u7a0d\u540e",
|
||||
"minutesShort": "{minutes} \u5206\u949f",
|
||||
"reflectionSaved": "复盘已保存",
|
||||
"reflectionPromptTitle": "复盘一下",
|
||||
"reflectionPromptBody": "写一句有用的总结。",
|
||||
"progressIntention": "目标",
|
||||
"progressFocus": "专注",
|
||||
"progressHabits": "习惯",
|
||||
"progressReflect": "复盘",
|
||||
"done": "完成",
|
||||
"next": "下一步",
|
||||
"later": "稍后",
|
||||
"minutesShort": "{minutes} 分钟",
|
||||
"@minutesShort": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -90,7 +103,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dailyStepsComplete": "\u4eca\u65e5\u5b8c\u6210 {completed}/4",
|
||||
"dailyStepsComplete": "今日完成 {completed}/3",
|
||||
"@dailyStepsComplete": {
|
||||
"placeholders": {
|
||||
"completed": {
|
||||
@@ -98,94 +111,100 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"habitOptions": "\u4e60\u60ef\u9009\u9879",
|
||||
"rename": "\u91cd\u547d\u540d",
|
||||
"delete": "\u5220\u9664",
|
||||
"habitsTitle": "\u4e60\u60ef",
|
||||
"habitsSubtitle": "\u5c11\u91cf\u3001\u53ef\u91cd\u590d\u3002",
|
||||
"emptyHabitsTitle": "\u6dfb\u52a0\u5c0f\u4e60\u60ef",
|
||||
"addHabitTitle": "\u6dfb\u52a0\u4e60\u60ef",
|
||||
"renameHabitTitle": "\u91cd\u547d\u540d\u4e60\u60ef",
|
||||
"habitHint": "\u4f8b\u5982\uff1a\u8bfb 10 \u9875\u4e66",
|
||||
"cancel": "\u53d6\u6d88",
|
||||
"save": "\u4fdd\u5b58",
|
||||
"setIntentionTitle": "\u8bbe\u7f6e\u76ee\u6807",
|
||||
"editIntentionTitle": "\u7f16\u8f91\u76ee\u6807",
|
||||
"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",
|
||||
"close": "\u5173\u95ed",
|
||||
"copy": "\u590d\u5236",
|
||||
"clearTodayDataTitle": "\u6e05\u7406\u4eca\u65e5\u6570\u636e\uff1f",
|
||||
"clearTodayDataBody": "\u53ea\u91cd\u7f6e\u4eca\u5929\u7684\u91cd\u70b9\u3001\u6253\u5361\u3001\u4e13\u6ce8\u8bb0\u5f55\u548c\u590d\u76d8\uff0c\u8fc7\u53bb\u65e5\u671f\u7684\u6570\u636e\u4f1a\u4fdd\u7559\u3002",
|
||||
"clearAllDataTitle": "\u5220\u9664\u5168\u90e8\u672c\u5730\u6570\u636e\uff1f",
|
||||
"clearAllDataBody": "\u4f1a\u5220\u9664\u672c\u673a\u7684\u76ee\u6807\u3001\u4e60\u60ef\u3001\u4e13\u6ce8\u8bb0\u5f55\u548c\u590d\u76d8\uff0c\u65e0\u6cd5\u64a4\u9500\u3002",
|
||||
"clear": "\u6e05\u9664",
|
||||
"deleteAll": "\u5168\u90e8\u5220\u9664",
|
||||
"privacyTitle": "\u9690\u79c1\u4e0e\u6570\u636e",
|
||||
"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\u968f\u65f6\u6e05\u7406\u4eca\u65e5\u8fdb\u5ea6\uff0c\u6216\u5220\u9664\u672c\u673a\u7684\u5168\u90e8\u672c\u5730\u6570\u636e\u3002",
|
||||
"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",
|
||||
"reflectionSheetSubtitle": "\u4fdd\u7559\u6709\u6548\u7684\uff0c\u8c03\u6574\u4e00\u4e2a\u70b9\u3002",
|
||||
"reflectionHint": "\u4e00\u53e5\u6709\u7528\u7684\u8bdd\u5c31\u591f\u3002",
|
||||
"savingLabel": "\u4fdd\u5b58\u4e2d",
|
||||
"saveReflection": "\u4fdd\u5b58\u590d\u76d8",
|
||||
"moodCalm": "\u5e73\u9759",
|
||||
"moodClear": "\u6e05\u9192",
|
||||
"moodProud": "\u6ee1\u610f",
|
||||
"moodTired": "\u75b2\u60eb",
|
||||
"meTitle": "\u6211\u7684\u82b1\u56ed",
|
||||
"meSubtitle": "\u504f\u597d\u548c\u8bb0\u5f55\u90fd\u53ea\u4fdd\u5b58\u5728\u672c\u673a\u3002",
|
||||
"appearance": "\u4e3b\u9898",
|
||||
"system": "\u7cfb\u7edf",
|
||||
"light": "\u6d45\u8272",
|
||||
"dark": "\u6df1\u8272",
|
||||
"language": "\u8bed\u8a00",
|
||||
"developmentOnly": "\u4ec5\u5f00\u53d1\u73af\u5883",
|
||||
"languageDefault": "\u9ed8\u8ba4",
|
||||
"habitOptions": "习惯选项",
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"habitsTitle": "辅助习惯",
|
||||
"habitsSubtitle": "可选的重复动作,今日重点仍然放在前面。",
|
||||
"emptyHabitsTitle": "添加一个辅助习惯",
|
||||
"addHabitTitle": "添加习惯",
|
||||
"renameHabitTitle": "重命名习惯",
|
||||
"habitHint": "例如:读 10 页书",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"textEntryRequired": "先写一个短句。",
|
||||
"setIntentionTitle": "设置今日重点",
|
||||
"editIntentionTitle": "编辑今日重点",
|
||||
"intentionHint": "今天最重要的是什么?",
|
||||
"deleteHabitTitle": "删除习惯?",
|
||||
"deleteHabitBody": "这个习惯的历史完成记录也会删除。",
|
||||
"close": "关闭",
|
||||
"copy": "复制",
|
||||
"clearTodayDataTitle": "清理今日数据?",
|
||||
"clearTodayDataBody": "只重置今天的重点、打卡、专注记录和复盘,过去日期的数据会保留。",
|
||||
"clearAllDataTitle": "删除全部本地数据?",
|
||||
"clearAllDataBody": "会删除本机的目标、习惯、专注记录和复盘,无法撤销。",
|
||||
"clear": "清除",
|
||||
"deleteAll": "全部删除",
|
||||
"privacyTitle": "隐私与数据",
|
||||
"privacyBodyOne": "Focus Seed 只在本机保存目标、习惯、专注记录、复盘和设置。当前版本没有登录、云同步、广告、分析 SDK 或付费 SDK。",
|
||||
"privacyBodyTwo": "你可以随时在设置中清理今日进度,或删除本机的全部本地数据。",
|
||||
"reflectionEmptyError": "写一句真实的总结。",
|
||||
"reflectionSaveError": "保存失败,请重试。",
|
||||
"reflectionSheetTitle": "留下一句进展",
|
||||
"reflectionSheetSubtitle": "记下刚推进了什么,下次就知道从哪里继续。",
|
||||
"reflectionHint": "刚才推进了什么?",
|
||||
"reflectionMoodLabel": "状态可以稍后补",
|
||||
"savingLabel": "保存中",
|
||||
"saveReflection": "保存记录",
|
||||
"moodCalm": "平静",
|
||||
"moodClear": "清醒",
|
||||
"moodProud": "满意",
|
||||
"moodTired": "疲惫",
|
||||
"meTitle": "我的",
|
||||
"meSubtitle": "管理你的节奏、外观和记录。",
|
||||
"appSettingsTitle": "应用设置",
|
||||
"appearance": "主题",
|
||||
"system": "系统",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"language": "语言",
|
||||
"developmentOnly": "仅开发环境",
|
||||
"languageDefault": "默认",
|
||||
"languageEnglish": "English",
|
||||
"languageChinese": "\u4e2d\u6587",
|
||||
"preferencesTitle": "\u504f\u597d",
|
||||
"preferencesDetail": "\u8ba9\u754c\u9762\u66f4\u987a\u624b",
|
||||
"localData": "\u672c\u5730\u8bb0\u5f55",
|
||||
"localDataDetail": "\u53ea\u5728\u672c\u673a",
|
||||
"personalGardenTitle": "\u4f60\u7684\u4e13\u6ce8\u82b1\u56ed",
|
||||
"personalGardenSubtitle": "\u6bcf\u4e00\u6b21\u5c0f\u4e13\u6ce8\uff0c\u90fd\u4f1a\u7559\u4e0b\u53ef\u56de\u770b\u7684\u75d5\u8ff9\u3002",
|
||||
"focusDataLabel": "\u4e13\u6ce8",
|
||||
"habitsDataLabel": "\u4e60\u60ef",
|
||||
"notesDataLabel": "\u8bb0\u5f55",
|
||||
"clearToday": "\u6e05\u7406\u4eca\u5929",
|
||||
"clearTodaySubtitle": "\u53ea\u91cd\u7f6e\u4eca\u5929\u7684\u95ed\u73af\u3002",
|
||||
"privacySubtitle": "\u65e0\u8d26\u53f7\uff0c\u65e0\u4e91\u540c\u6b65\u3002",
|
||||
"deleteAllLocalData": "\u5220\u9664\u5168\u90e8\u6570\u636e",
|
||||
"deleteAllLocalDataSubtitle": "\u79fb\u9664\u8fd9\u53f0\u8bbe\u5907\u4e0a\u7684 Focus Seed \u8bb0\u5f55\u3002",
|
||||
"todayDataCleared": "\u4eca\u65e5\u6570\u636e\u5df2\u6e05\u7406",
|
||||
"localDataCleared": "\u672c\u5730\u6570\u636e\u5df2\u5220\u9664",
|
||||
"journeyTitle": "\u8fdb\u5c55",
|
||||
"journeyOverviewLabel": "\u4eca\u65e5\u95ed\u73af",
|
||||
"journeyOverviewStartTitle": "\u5148\u5b9a\u4e0b\u4e00\u4e2a\u91cd\u70b9",
|
||||
"journeyOverviewStartBody": "\u5199\u4e0b\u4eca\u5929\u6700\u503c\u5f97\u5b8c\u6210\u7684\u90a3\u4ef6\u4e8b\u3002",
|
||||
"journeyOverviewActiveTitle": "\u4eca\u5929\u6b63\u5728\u6210\u5f62",
|
||||
"journeyOverviewActiveBody": "\u7ee7\u7eed\u63a8\u8fdb\uff1a\u4e13\u6ce8\u3001\u6253\u5361\uff0c\u7136\u540e\u7528\u4e00\u53e5\u8bdd\u590d\u76d8\u3002",
|
||||
"journeyOverviewDoneTitle": "\u4eca\u65e5\u95ed\u73af\u5b8c\u6210",
|
||||
"journeyOverviewDoneBody": "\u4f60\u5df2\u7ecf\u7ed9\u672a\u6765\u7684\u81ea\u5df1\u7559\u4e0b\u4e86\u4e00\u6761\u6e05\u6670\u8bb0\u5f55\u3002",
|
||||
"journeyOverviewProgress": "\u5df2\u5b8c\u6210 {completed}/4",
|
||||
"@journeyOverviewProgress": {
|
||||
"languageChinese": "中文",
|
||||
"preferencesTitle": "偏好",
|
||||
"preferencesDetail": "主题、语言和默认专注",
|
||||
"defaultFocusDuration": "默认专注",
|
||||
"defaultFocusDurationDetail": "下一轮",
|
||||
"localData": "本地记录",
|
||||
"localDataDetail": "只在本机",
|
||||
"personalGardenTitle": "我的累计",
|
||||
"personalGardenSubtitle": "你的本地行动记录、进展和应用控制都保留在这台设备上。",
|
||||
"focusDataLabel": "专注",
|
||||
"habitsDataLabel": "习惯",
|
||||
"notesDataLabel": "进展",
|
||||
"totalActionTimeLabel": "行动时间",
|
||||
"recordsCountLabel": "记录",
|
||||
"progressNotesCountLabel": "进展",
|
||||
"clearToday": "清理今天",
|
||||
"clearTodaySubtitle": "只重置今天的闭环。",
|
||||
"privacySubtitle": "无账号,无云同步。",
|
||||
"deleteAllLocalData": "删除全部数据",
|
||||
"deleteAllLocalDataSubtitle": "移除这台设备上的 Focus Seed 记录。",
|
||||
"todayDataCleared": "今日数据已清理",
|
||||
"localDataCleared": "本地数据已删除",
|
||||
"todayLoopLabel": "今日闭环",
|
||||
"todayLoopStartTitle": "先定下一个重点",
|
||||
"todayLoopStartBody": "先写下今天最重要的一件事。",
|
||||
"todayLoopActiveTitle": "今天正在成形",
|
||||
"todayLoopActiveBody": "先完成一轮专注,再留一句有用的记录。",
|
||||
"todayLoopDoneTitle": "进展已保存",
|
||||
"todayLoopDoneBody": "今天已经推进了一点,下次也知道从哪里继续。",
|
||||
"todayLoopProgress": "已完成 {completed}/3",
|
||||
"@todayLoopProgress": {
|
||||
"placeholders": {
|
||||
"completed": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reviewNow": "\u73b0\u5728\u590d\u76d8",
|
||||
"journeyStatsTitle": "7 \u65e5\u8282\u594f",
|
||||
"weekFocusLabel": "\u4e13\u6ce8",
|
||||
"streakLabel": "\u8fde\u7eed",
|
||||
"activeDaysLabel": "\u6d3b\u8dc3",
|
||||
"habitRateLabel": "\u4e60\u60ef",
|
||||
"daysValue": "{count}\u5929",
|
||||
"reviewNow": "写一句记录",
|
||||
"weekFocusLabel": "专注",
|
||||
"streakLabel": "连续",
|
||||
"activeDaysLabel": "活跃",
|
||||
"habitRateLabel": "习惯",
|
||||
"daysValue": "{count}天",
|
||||
"@daysValue": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -201,17 +220,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewHistory": "\u8bb0\u5f55\u5e93",
|
||||
"viewHistorySubtitle": "\u67e5\u770b\u4e13\u6ce8\u8bb0\u5f55\u548c\u590d\u76d8\u3002",
|
||||
"historyTitle": "\u5386\u53f2",
|
||||
"focusHistory": "\u4e13\u6ce8",
|
||||
"notesHistory": "\u590d\u76d8",
|
||||
"emptyFocusHistoryTitle": "\u8fd8\u6ca1\u6709\u4e13\u6ce8\u8bb0\u5f55",
|
||||
"emptyFocusHistoryBody": "\u4ece\u4eca\u65e5\u9875\u5f00\u59cb\u4e00\u8f6e\u4e13\u6ce8\uff0c\u5b8c\u6210\u540e\u4f1a\u51fa\u73b0\u5728\u8fd9\u91cc\u3002",
|
||||
"emptyNotesHistoryTitle": "\u8fd8\u6ca1\u6709\u590d\u76d8",
|
||||
"emptyNotesHistoryBody": "\u4e13\u6ce8\u540e\u4fdd\u5b58\u4e00\u53e5\u590d\u76d8\uff0c\u8ba9\u8fdb\u5c55\u66f4\u6e05\u6670\u3002",
|
||||
"recentFocus": "\u6700\u8fd1\u4e13\u6ce8",
|
||||
"sessionsTotal": "\u5171 {count} \u6b21",
|
||||
"viewHistory": "记录库",
|
||||
"viewHistorySubtitle": "查看专注记录和复盘。",
|
||||
"historyTitle": "历史",
|
||||
"focusHistory": "专注",
|
||||
"notesHistory": "复盘",
|
||||
"emptyFocusHistoryTitle": "还没有专注记录",
|
||||
"emptyFocusHistoryBody": "完成一轮专注后,记录会出现在这里。",
|
||||
"emptyNotesHistoryTitle": "还没有复盘",
|
||||
"emptyNotesHistoryBody": "专注后留一句复盘,这里会保留它。",
|
||||
"recordsEmptyTitle": "还没有进展记录",
|
||||
"recordsEmptyBody": "完成一次专注,再留一句记录,这里就会保留今天的进展。",
|
||||
"recordsCalendarTitle": "活动日期",
|
||||
"recordsDateLedgerTitle": "当日账本",
|
||||
"backToToday": "回到今日",
|
||||
"recordsNoIntention": "未命名今日重点",
|
||||
"recordsNoReflection": "还没有进展记录",
|
||||
"recordsMissingProgressHelper": "补一句,之后更容易接上。",
|
||||
"addProgressNote": "补一句进展",
|
||||
"continueThisAction": "继续这件事",
|
||||
"replaceTodayActionTitle": "替换今天的行动?",
|
||||
"replaceTodayActionBody": "会把今天的行动改为“{actionTitle}”,已有记录不会删除。",
|
||||
"@replaceTodayActionBody": {
|
||||
"placeholders": {
|
||||
"actionTitle": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recentFocus": "最近专注",
|
||||
"sessionsTotal": "共 {count} 次",
|
||||
"@sessionsTotal": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -219,7 +257,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"focusMinutes": "\u4e13\u6ce8 {minutes} \u5206\u949f",
|
||||
"focusMinutes": "专注 {minutes} 分钟",
|
||||
"@focusMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -227,9 +265,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"recentReflections": "\u6700\u8fd1\u590d\u76d8",
|
||||
"recentReflectionsDetail": "\u503c\u5f97\u4fdd\u7559\u7684\u8bb0\u5f55",
|
||||
"focusedMinutes": "\u5df2\u4e13\u6ce8 {minutes} \u5206\u949f",
|
||||
"recentReflections": "最近复盘",
|
||||
"recentReflectionsDetail": "值得保留的记录",
|
||||
"focusedMinutes": "已专注 {minutes} 分钟",
|
||||
"@focusedMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -237,8 +275,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"noFocusSessionYet": "\u8fd8\u6ca1\u6709\u5b8c\u6210\u4e13\u6ce8",
|
||||
"habitsCompleted": "{completed}/{total} \u5b8c\u6210",
|
||||
"noFocusSessionYet": "还没有完成专注",
|
||||
"habitsCompleted": "{completed}/{total} 完成",
|
||||
"@habitsCompleted": {
|
||||
"placeholders": {
|
||||
"completed": {
|
||||
@@ -249,33 +287,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"reflectionCaptured": "\u4eca\u65e5\u590d\u76d8\u5df2\u8bb0\u5f55",
|
||||
"reflectionAction": "\u7528\u4e00\u53e5\u8bdd\u6536\u5c3e",
|
||||
"growthTitle": "\u6210\u957f",
|
||||
"growthFlowerOpen": "\u4eca\u65e5\u82b1\u6735\u5df2\u5f00\u653e",
|
||||
"growthLocked": "\u590d\u76d8\u540e\u89e3\u9501",
|
||||
"sevenDayGrowth": "7 \u65e5\u6210\u957f",
|
||||
"sevenDayGrowthSubtitle": "\u8fde\u7eed\u884c\u52a8\u4f1a\u88ab\u770b\u89c1\u3002",
|
||||
"growthSeed": "\u79cd\u5b50",
|
||||
"growthSprout": "\u53d1\u82bd",
|
||||
"growthLeaves": "\u751f\u957f",
|
||||
"growthFlower": "\u5f00\u82b1",
|
||||
"growthDaySemantic": "{date}\uff0c{stage}",
|
||||
"@growthDaySemantic": {
|
||||
"reflectionCaptured": "今日复盘已记录",
|
||||
"reflectionAction": "用一句话给今天收尾",
|
||||
"nextActionTitle": "下一步",
|
||||
"nextActionSetFocusTitle": "写下今天的重点",
|
||||
"nextActionSetFocusBody": "一个短句就够了,之后还可以调整。",
|
||||
"nextActionStartFocusTitle": "开始 10 分钟",
|
||||
"nextActionStartFocusBody": "写不写都可以,先开始。",
|
||||
"nextActionStartActionBody": "先推进一小段,完成后留一句进展。",
|
||||
"nextActionHabitsTitle": "完成一个小习惯",
|
||||
"nextActionHabitsBody": "趁着还顺手,先做掉一个可重复的小动作。",
|
||||
"nextActionReflectTitle": "写一句记录",
|
||||
"nextActionReflectBody": "记下刚才推进了什么,下次更容易继续。",
|
||||
"nextActionCompleteTitle": "今天已经推进了一点",
|
||||
"nextActionCompleteBody": "进展已经留下,想继续时再来一段。",
|
||||
"nextActionContinueFocus": "再专注 {minutes} 分钟",
|
||||
"@nextActionContinueFocus": {
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"type": "Object"
|
||||
},
|
||||
"stage": {
|
||||
"type": "Object"
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"growthSeedSemantic": "\u7b49\u5f85\u4eca\u65e5\u76ee\u6807\u7684\u79cd\u5b50",
|
||||
"growthSproutSemantic": "\u76ee\u6807\u5df2\u79cd\u4e0b\uff0c\u6b63\u5728\u53d1\u82bd",
|
||||
"growthLeavesSemantic": "\u4eca\u65e5\u8fdb\u5ea6\u8ba9\u690d\u7269\u751f\u957f",
|
||||
"growthFlowerSemantic": "\u4eca\u65e5\u95ed\u73af\u5b8c\u6210\uff0c\u82b1\u6735\u5f00\u653e",
|
||||
"defaultHabitPlanTask": "\u8ba1\u5212\u4e00\u4e2a\u660e\u786e\u4efb\u52a1",
|
||||
"defaultHabitFocus": "\u4e13\u6ce8 25 \u5206\u949f",
|
||||
"defaultHabitReflect": "\u5199\u4e00\u53e5\u590d\u76d8"
|
||||
"habitTemplatesTitle": "可以直接开始",
|
||||
"habitTemplatesBody": "添加一个很小但能重复的动作。",
|
||||
"habitTemplateStretch": "2 分钟拉伸",
|
||||
"habitTemplateRead": "读 10 页书",
|
||||
"habitTemplateWalk": "出门走走",
|
||||
"habitTemplateNoScroll": "午餐不刷手机",
|
||||
"languageDetail": "界面语言",
|
||||
"recordsAndPrivacyTitle": "记录与隐私",
|
||||
"recordsAndPrivacyBody": "历史只保存在本机。你可以保留、重置或彻底删除。",
|
||||
"welcomeTrustLabel": "今日",
|
||||
"welcomeTitle": "每天只种一件事。",
|
||||
"welcomeBody": "先选今天最重要的一件。",
|
||||
"welcomeNext": "下一步会从这里开始。",
|
||||
"onboardingFocusBadge": "专注",
|
||||
"onboardingFocusTitle": "先守住 10 分钟。",
|
||||
"onboardingFocusBody": "时间不用长,能开始就有进展。",
|
||||
"onboardingFocusNext": "专注时,把这件事留在眼前。",
|
||||
"onboardingReflectBadge": "记录",
|
||||
"onboardingReflectTitle": "留下一句进展。",
|
||||
"onboardingReflectBody": "结束时记一句,下次就知道从哪里继续。",
|
||||
"onboardingReflectNext": "现在设置今天重点。",
|
||||
"onboardingNext": "继续",
|
||||
"welcomeStart": "设置今天重点",
|
||||
"welcomeSkip": "跳过",
|
||||
"landingTitle": "先开始 10 分钟。",
|
||||
"landingBody": "写不写都可以,先开始。",
|
||||
"landingFocusLabel": "今天想先做什么?",
|
||||
"landingFocusEntry": "写下这次要做的事",
|
||||
"landingFocusHint": "例如:完成上线检查清单",
|
||||
"customFocusDuration": "自定义时长",
|
||||
"customFocusDurationHint": "输入 1-180 分钟",
|
||||
"minutesInputLabel": "分钟数",
|
||||
"landingStart": "开始 10 分钟",
|
||||
"landingSkip": "进入今日页",
|
||||
"defaultHabitPlanTask": "计划一个明确任务",
|
||||
"defaultHabitFocus": "专注 10 分钟",
|
||||
"defaultHabitReflect": "写一句复盘"
|
||||
}
|
||||
|
||||
@@ -125,11 +125,11 @@ abstract class AppLocalizations {
|
||||
/// **'Today'**
|
||||
String get navToday;
|
||||
|
||||
/// No description provided for @navJourney.
|
||||
/// No description provided for @navRecords.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Journey'**
|
||||
String get navJourney;
|
||||
/// **'Records'**
|
||||
String get navRecords;
|
||||
|
||||
/// No description provided for @navMe.
|
||||
///
|
||||
@@ -149,10 +149,16 @@ abstract class AppLocalizations {
|
||||
/// **'Could not open Focus Seed.'**
|
||||
String get couldNotOpenApp;
|
||||
|
||||
/// No description provided for @loadingLocalData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Loading local data'**
|
||||
String get loadingLocalData;
|
||||
|
||||
/// No description provided for @openFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your local garden could not be opened.'**
|
||||
/// **'Your local data could not be opened.'**
|
||||
String get openFailed;
|
||||
|
||||
/// No description provided for @saveFailed.
|
||||
@@ -233,12 +239,30 @@ abstract class AppLocalizations {
|
||||
/// **'Session complete'**
|
||||
String get timerCompleted;
|
||||
|
||||
/// No description provided for @focusCurrentIntention.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Today\'s focus: {intention}'**
|
||||
String focusCurrentIntention(String intention);
|
||||
|
||||
/// No description provided for @focusNoIntentionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start this session first'**
|
||||
String get focusNoIntentionTitle;
|
||||
|
||||
/// No description provided for @reflectOneMinute.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reflect for 1 min'**
|
||||
/// **'Write progress note'**
|
||||
String get reflectOneMinute;
|
||||
|
||||
/// No description provided for @startFocus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start focus'**
|
||||
String get startFocus;
|
||||
|
||||
/// No description provided for @startAnotherSession.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -263,6 +287,18 @@ abstract class AppLocalizations {
|
||||
/// **'End'**
|
||||
String get end;
|
||||
|
||||
/// No description provided for @completeSession.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Complete session'**
|
||||
String get completeSession;
|
||||
|
||||
/// No description provided for @continueSession.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Continue session'**
|
||||
String get continueSession;
|
||||
|
||||
/// No description provided for @resume.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -314,7 +350,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @emptyHabitPrompt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add one repeatable habit.'**
|
||||
/// **'Keep this optional and small.'**
|
||||
String get emptyHabitPrompt;
|
||||
|
||||
/// No description provided for @viewAllHabits.
|
||||
@@ -392,7 +428,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @dailyStepsComplete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{completed}/4 daily steps complete'**
|
||||
/// **'{completed}/3 daily steps complete'**
|
||||
String dailyStepsComplete(int completed);
|
||||
|
||||
/// No description provided for @habitOptions.
|
||||
@@ -416,19 +452,19 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @habitsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Habits'**
|
||||
/// **'Supporting habits'**
|
||||
String get habitsTitle;
|
||||
|
||||
/// No description provided for @habitsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep it small and repeatable.'**
|
||||
/// **'Optional repeatable steps. Today\'s focus still comes first.'**
|
||||
String get habitsSubtitle;
|
||||
|
||||
/// No description provided for @emptyHabitsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a small habit'**
|
||||
/// **'Add a supporting habit'**
|
||||
String get emptyHabitsTitle;
|
||||
|
||||
/// No description provided for @addHabitTitle.
|
||||
@@ -461,16 +497,22 @@ abstract class AppLocalizations {
|
||||
/// **'Save'**
|
||||
String get save;
|
||||
|
||||
/// No description provided for @textEntryRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Write a short line first.'**
|
||||
String get textEntryRequired;
|
||||
|
||||
/// No description provided for @setIntentionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set intention'**
|
||||
/// **'Set today\'s focus'**
|
||||
String get setIntentionTitle;
|
||||
|
||||
/// No description provided for @editIntentionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit intention'**
|
||||
/// **'Edit today\'s focus'**
|
||||
String get editIntentionTitle;
|
||||
|
||||
/// No description provided for @intentionHint.
|
||||
@@ -554,7 +596,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @privacyBodyTwo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You can clear today\'s progress or delete all local data from Me at any time.'**
|
||||
/// **'You can clear today\'s progress or delete all local data from settings at any time.'**
|
||||
String get privacyBodyTwo;
|
||||
|
||||
/// No description provided for @reflectionEmptyError.
|
||||
@@ -572,21 +614,27 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @reflectionSheetTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'How did it go?'**
|
||||
/// **'Leave one progress note'**
|
||||
String get reflectionSheetTitle;
|
||||
|
||||
/// No description provided for @reflectionSheetSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep what worked. Adjust one thing.'**
|
||||
/// **'Save what moved forward so next time has a clear start.'**
|
||||
String get reflectionSheetSubtitle;
|
||||
|
||||
/// No description provided for @reflectionHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'One useful sentence is enough.'**
|
||||
/// **'What moved forward?'**
|
||||
String get reflectionHint;
|
||||
|
||||
/// No description provided for @reflectionMoodLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mood is optional'**
|
||||
String get reflectionMoodLabel;
|
||||
|
||||
/// No description provided for @savingLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -596,7 +644,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @saveReflection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save reflection'**
|
||||
/// **'Save note'**
|
||||
String get saveReflection;
|
||||
|
||||
/// No description provided for @moodCalm.
|
||||
@@ -626,15 +674,21 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @meTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your garden'**
|
||||
/// **'Me'**
|
||||
String get meTitle;
|
||||
|
||||
/// No description provided for @meSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Preferences and records stay on this device.'**
|
||||
/// **'Manage your rhythm, appearance, and records.'**
|
||||
String get meSubtitle;
|
||||
|
||||
/// No description provided for @appSettingsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'App settings'**
|
||||
String get appSettingsTitle;
|
||||
|
||||
/// No description provided for @appearance.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -698,9 +752,21 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @preferencesDetail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Make it feel right'**
|
||||
/// **'Theme, language, and default focus'**
|
||||
String get preferencesDetail;
|
||||
|
||||
/// No description provided for @defaultFocusDuration.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Default focus'**
|
||||
String get defaultFocusDuration;
|
||||
|
||||
/// No description provided for @defaultFocusDurationDetail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Next session'**
|
||||
String get defaultFocusDurationDetail;
|
||||
|
||||
/// No description provided for @localData.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -716,13 +782,13 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @personalGardenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your focus garden'**
|
||||
/// **'My accumulation'**
|
||||
String get personalGardenTitle;
|
||||
|
||||
/// No description provided for @personalGardenSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Every small session leaves a trace you can return to.'**
|
||||
/// **'Your local action records, progress notes, and app controls stay on this device.'**
|
||||
String get personalGardenSubtitle;
|
||||
|
||||
/// No description provided for @focusDataLabel.
|
||||
@@ -740,9 +806,27 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @notesDataLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Notes'**
|
||||
/// **'Progress'**
|
||||
String get notesDataLabel;
|
||||
|
||||
/// No description provided for @totalActionTimeLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Action time'**
|
||||
String get totalActionTimeLabel;
|
||||
|
||||
/// No description provided for @recordsCountLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Records'**
|
||||
String get recordsCountLabel;
|
||||
|
||||
/// No description provided for @progressNotesCountLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Progress notes'**
|
||||
String get progressNotesCountLabel;
|
||||
|
||||
/// No description provided for @clearToday.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -785,72 +869,60 @@ abstract class AppLocalizations {
|
||||
/// **'Local data deleted'**
|
||||
String get localDataCleared;
|
||||
|
||||
/// No description provided for @journeyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Progress'**
|
||||
String get journeyTitle;
|
||||
|
||||
/// No description provided for @journeyOverviewLabel.
|
||||
/// No description provided for @todayLoopLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TODAY\'S LOOP'**
|
||||
String get journeyOverviewLabel;
|
||||
String get todayLoopLabel;
|
||||
|
||||
/// No description provided for @journeyOverviewStartTitle.
|
||||
/// No description provided for @todayLoopStartTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start with one clear focus'**
|
||||
String get journeyOverviewStartTitle;
|
||||
String get todayLoopStartTitle;
|
||||
|
||||
/// No description provided for @journeyOverviewStartBody.
|
||||
/// No description provided for @todayLoopStartBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pick the thing that would make today feel finished.'**
|
||||
String get journeyOverviewStartBody;
|
||||
/// **'Pick the one result that matters today.'**
|
||||
String get todayLoopStartBody;
|
||||
|
||||
/// No description provided for @journeyOverviewActiveTitle.
|
||||
/// No description provided for @todayLoopActiveTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your day is taking shape'**
|
||||
String get journeyOverviewActiveTitle;
|
||||
String get todayLoopActiveTitle;
|
||||
|
||||
/// No description provided for @journeyOverviewActiveBody.
|
||||
/// No description provided for @todayLoopActiveBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep the loop moving: focus, habits, then a short reflection.'**
|
||||
String get journeyOverviewActiveBody;
|
||||
/// **'Focus once, then leave one useful note.'**
|
||||
String get todayLoopActiveBody;
|
||||
|
||||
/// No description provided for @journeyOverviewDoneTitle.
|
||||
/// No description provided for @todayLoopDoneTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Loop complete'**
|
||||
String get journeyOverviewDoneTitle;
|
||||
/// **'Progress saved'**
|
||||
String get todayLoopDoneTitle;
|
||||
|
||||
/// No description provided for @journeyOverviewDoneBody.
|
||||
/// No description provided for @todayLoopDoneBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You left a clean record for future you.'**
|
||||
String get journeyOverviewDoneBody;
|
||||
/// **'You moved today forward and left a clear place to continue.'**
|
||||
String get todayLoopDoneBody;
|
||||
|
||||
/// No description provided for @journeyOverviewProgress.
|
||||
/// No description provided for @todayLoopProgress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{completed}/4 done'**
|
||||
String journeyOverviewProgress(int completed);
|
||||
/// **'{completed}/3 done'**
|
||||
String todayLoopProgress(int completed);
|
||||
|
||||
/// No description provided for @reviewNow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reflect now'**
|
||||
/// **'Write a note'**
|
||||
String get reviewNow;
|
||||
|
||||
/// No description provided for @journeyStatsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'7-day rhythm'**
|
||||
String get journeyStatsTitle;
|
||||
|
||||
/// No description provided for @weekFocusLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -926,7 +998,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @emptyFocusHistoryBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start one session from Today and it will appear here.'**
|
||||
/// **'Finish one focus session and it will appear here.'**
|
||||
String get emptyFocusHistoryBody;
|
||||
|
||||
/// No description provided for @emptyNotesHistoryTitle.
|
||||
@@ -938,9 +1010,81 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @emptyNotesHistoryBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save a short reflection after focus to make your progress visible.'**
|
||||
/// **'Save one reflection after a session and it will appear here.'**
|
||||
String get emptyNotesHistoryBody;
|
||||
|
||||
/// No description provided for @recordsEmptyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No progress records yet'**
|
||||
String get recordsEmptyTitle;
|
||||
|
||||
/// No description provided for @recordsEmptyBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finish one focus session and leave a note to keep today\'s progress here.'**
|
||||
String get recordsEmptyBody;
|
||||
|
||||
/// No description provided for @recordsCalendarTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Activity dates'**
|
||||
String get recordsCalendarTitle;
|
||||
|
||||
/// No description provided for @recordsDateLedgerTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Date ledger'**
|
||||
String get recordsDateLedgerTitle;
|
||||
|
||||
/// No description provided for @backToToday.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Back to Today'**
|
||||
String get backToToday;
|
||||
|
||||
/// No description provided for @recordsNoIntention.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unnamed daily focus'**
|
||||
String get recordsNoIntention;
|
||||
|
||||
/// No description provided for @recordsNoReflection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No progress note yet'**
|
||||
String get recordsNoReflection;
|
||||
|
||||
/// No description provided for @recordsMissingProgressHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add one line so this is easier to pick up later.'**
|
||||
String get recordsMissingProgressHelper;
|
||||
|
||||
/// No description provided for @addProgressNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add progress note'**
|
||||
String get addProgressNote;
|
||||
|
||||
/// No description provided for @continueThisAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Continue this'**
|
||||
String get continueThisAction;
|
||||
|
||||
/// No description provided for @replaceTodayActionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Replace today\'s action?'**
|
||||
String get replaceTodayActionTitle;
|
||||
|
||||
/// No description provided for @replaceTodayActionBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This will change today\'s action to \"{actionTitle}\". Existing records will stay.'**
|
||||
String replaceTodayActionBody(String actionTitle);
|
||||
|
||||
/// No description provided for @recentFocus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -998,92 +1142,290 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @reflectionAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Close the loop with one sentence'**
|
||||
/// **'Write one sentence to close today'**
|
||||
String get reflectionAction;
|
||||
|
||||
/// No description provided for @growthTitle.
|
||||
/// No description provided for @nextActionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Growth'**
|
||||
String get growthTitle;
|
||||
/// **'Next action'**
|
||||
String get nextActionTitle;
|
||||
|
||||
/// No description provided for @growthFlowerOpen.
|
||||
/// No description provided for @nextActionSetFocusTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Today\'s flower is open'**
|
||||
String get growthFlowerOpen;
|
||||
/// **'Write today\'s focus'**
|
||||
String get nextActionSetFocusTitle;
|
||||
|
||||
/// No description provided for @growthLocked.
|
||||
/// No description provided for @nextActionSetFocusBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unlocked after reflection'**
|
||||
String get growthLocked;
|
||||
/// **'One short line is enough. You can adjust it later.'**
|
||||
String get nextActionSetFocusBody;
|
||||
|
||||
/// No description provided for @sevenDayGrowth.
|
||||
/// No description provided for @nextActionStartFocusTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'7-day growth'**
|
||||
String get sevenDayGrowth;
|
||||
/// **'Start 10 minutes'**
|
||||
String get nextActionStartFocusTitle;
|
||||
|
||||
/// No description provided for @sevenDayGrowthSubtitle.
|
||||
/// No description provided for @nextActionStartFocusBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Small actions become visible over time.'**
|
||||
String get sevenDayGrowthSubtitle;
|
||||
/// **'Writing the action is optional. Start first.'**
|
||||
String get nextActionStartFocusBody;
|
||||
|
||||
/// No description provided for @growthSeed.
|
||||
/// No description provided for @nextActionStartActionBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'seed'**
|
||||
String get growthSeed;
|
||||
/// **'Move this forward in one small session, then leave one progress note.'**
|
||||
String get nextActionStartActionBody;
|
||||
|
||||
/// No description provided for @growthSprout.
|
||||
/// No description provided for @nextActionHabitsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'sprout'**
|
||||
String get growthSprout;
|
||||
/// **'Finish one small habit'**
|
||||
String get nextActionHabitsTitle;
|
||||
|
||||
/// No description provided for @growthLeaves.
|
||||
/// No description provided for @nextActionHabitsBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'leaves'**
|
||||
String get growthLeaves;
|
||||
/// **'Complete one repeatable step while it is still easy.'**
|
||||
String get nextActionHabitsBody;
|
||||
|
||||
/// No description provided for @growthFlower.
|
||||
/// No description provided for @nextActionReflectTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'flower'**
|
||||
String get growthFlower;
|
||||
/// **'Write a quick note'**
|
||||
String get nextActionReflectTitle;
|
||||
|
||||
/// No description provided for @growthDaySemantic.
|
||||
/// No description provided for @nextActionReflectBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{date}, {stage}'**
|
||||
String growthDaySemantic(Object date, Object stage);
|
||||
/// **'Record what moved forward so the next start is easier.'**
|
||||
String get nextActionReflectBody;
|
||||
|
||||
/// No description provided for @growthSeedSemantic.
|
||||
/// No description provided for @nextActionCompleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A seed waiting for today\'s intention'**
|
||||
String get growthSeedSemantic;
|
||||
/// **'Today moved forward'**
|
||||
String get nextActionCompleteTitle;
|
||||
|
||||
/// No description provided for @growthSproutSemantic.
|
||||
/// No description provided for @nextActionCompleteBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A sprout growing from today\'s intention'**
|
||||
String get growthSproutSemantic;
|
||||
/// **'Your note is saved. Continue only if it still feels useful.'**
|
||||
String get nextActionCompleteBody;
|
||||
|
||||
/// No description provided for @growthLeavesSemantic.
|
||||
/// No description provided for @nextActionContinueFocus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A young plant growing with today\'s progress'**
|
||||
String get growthLeavesSemantic;
|
||||
/// **'Focus {minutes} min again'**
|
||||
String nextActionContinueFocus(int minutes);
|
||||
|
||||
/// No description provided for @growthFlowerSemantic.
|
||||
/// No description provided for @habitTemplatesTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A flower marking a complete daily cycle'**
|
||||
String get growthFlowerSemantic;
|
||||
/// **'Useful starts'**
|
||||
String get habitTemplatesTitle;
|
||||
|
||||
/// No description provided for @habitTemplatesBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add one tiny repeatable action.'**
|
||||
String get habitTemplatesBody;
|
||||
|
||||
/// No description provided for @habitTemplateStretch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'2 min stretch'**
|
||||
String get habitTemplateStretch;
|
||||
|
||||
/// No description provided for @habitTemplateRead.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Read 10 pages'**
|
||||
String get habitTemplateRead;
|
||||
|
||||
/// No description provided for @habitTemplateWalk.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Walk outside'**
|
||||
String get habitTemplateWalk;
|
||||
|
||||
/// No description provided for @habitTemplateNoScroll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No-scroll lunch'**
|
||||
String get habitTemplateNoScroll;
|
||||
|
||||
/// No description provided for @languageDetail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Interface language'**
|
||||
String get languageDetail;
|
||||
|
||||
/// No description provided for @recordsAndPrivacyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Records & privacy'**
|
||||
String get recordsAndPrivacyTitle;
|
||||
|
||||
/// No description provided for @recordsAndPrivacyBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your history stays local. Keep, reset, or delete it when needed.'**
|
||||
String get recordsAndPrivacyBody;
|
||||
|
||||
/// No description provided for @welcomeTrustLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TODAY'**
|
||||
String get welcomeTrustLabel;
|
||||
|
||||
/// No description provided for @welcomeTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Plant just one thing each day.'**
|
||||
String get welcomeTitle;
|
||||
|
||||
/// No description provided for @welcomeBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start with today\'s most important thing.'**
|
||||
String get welcomeBody;
|
||||
|
||||
/// No description provided for @welcomeNext.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The next step starts here.'**
|
||||
String get welcomeNext;
|
||||
|
||||
/// No description provided for @onboardingFocusBadge.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'FOCUS'**
|
||||
String get onboardingFocusBadge;
|
||||
|
||||
/// No description provided for @onboardingFocusTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Protect 10 minutes first.'**
|
||||
String get onboardingFocusTitle;
|
||||
|
||||
/// No description provided for @onboardingFocusBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'It does not have to be long. Starting creates progress.'**
|
||||
String get onboardingFocusBody;
|
||||
|
||||
/// No description provided for @onboardingFocusNext.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep this one thing in front of you while you focus.'**
|
||||
String get onboardingFocusNext;
|
||||
|
||||
/// No description provided for @onboardingReflectBadge.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'NOTE'**
|
||||
String get onboardingReflectBadge;
|
||||
|
||||
/// No description provided for @onboardingReflectTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Leave one progress note.'**
|
||||
String get onboardingReflectTitle;
|
||||
|
||||
/// No description provided for @onboardingReflectBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'When you finish, write one line so you know where to continue next time.'**
|
||||
String get onboardingReflectBody;
|
||||
|
||||
/// No description provided for @onboardingReflectNext.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Now set today\'s focus.'**
|
||||
String get onboardingReflectNext;
|
||||
|
||||
/// No description provided for @onboardingNext.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Continue'**
|
||||
String get onboardingNext;
|
||||
|
||||
/// No description provided for @welcomeStart.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set today\'s focus'**
|
||||
String get welcomeStart;
|
||||
|
||||
/// No description provided for @welcomeSkip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Skip'**
|
||||
String get welcomeSkip;
|
||||
|
||||
/// No description provided for @landingTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start with 10 minutes.'**
|
||||
String get landingTitle;
|
||||
|
||||
/// No description provided for @landingBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Writing is optional. Start first.'**
|
||||
String get landingBody;
|
||||
|
||||
/// No description provided for @landingFocusLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'What do you want to do first?'**
|
||||
String get landingFocusLabel;
|
||||
|
||||
/// No description provided for @landingFocusEntry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Write down what you will do'**
|
||||
String get landingFocusEntry;
|
||||
|
||||
/// No description provided for @landingFocusHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Example: finish the launch checklist'**
|
||||
String get landingFocusHint;
|
||||
|
||||
/// No description provided for @customFocusDuration.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Custom length'**
|
||||
String get customFocusDuration;
|
||||
|
||||
/// No description provided for @customFocusDurationHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter 1-180 minutes'**
|
||||
String get customFocusDurationHint;
|
||||
|
||||
/// No description provided for @minutesInputLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Minutes'**
|
||||
String get minutesInputLabel;
|
||||
|
||||
/// No description provided for @landingStart.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start 10 minutes'**
|
||||
String get landingStart;
|
||||
|
||||
/// No description provided for @landingSkip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter Today'**
|
||||
String get landingSkip;
|
||||
|
||||
/// No description provided for @defaultHabitPlanTask.
|
||||
///
|
||||
@@ -1094,7 +1436,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @defaultHabitFocus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Focus for 25 minutes'**
|
||||
/// **'Focus for 10 minutes'**
|
||||
String get defaultHabitFocus;
|
||||
|
||||
/// No description provided for @defaultHabitReflect.
|
||||
|
||||
@@ -24,7 +24,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get navToday => 'Today';
|
||||
|
||||
@override
|
||||
String get navJourney => 'Journey';
|
||||
String get navRecords => 'Records';
|
||||
|
||||
@override
|
||||
String get navMe => 'Me';
|
||||
@@ -36,7 +36,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get couldNotOpenApp => 'Could not open Focus Seed.';
|
||||
|
||||
@override
|
||||
String get openFailed => 'Your local garden could not be opened.';
|
||||
String get loadingLocalData => 'Loading local data';
|
||||
|
||||
@override
|
||||
String get openFailed => 'Your local data could not be opened.';
|
||||
|
||||
@override
|
||||
String get saveFailed => 'That change could not be saved. Please try again.';
|
||||
@@ -80,7 +83,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get timerCompleted => 'Session complete';
|
||||
|
||||
@override
|
||||
String get reflectOneMinute => 'Reflect for 1 min';
|
||||
String focusCurrentIntention(String intention) {
|
||||
return 'Today\'s focus: $intention';
|
||||
}
|
||||
|
||||
@override
|
||||
String get focusNoIntentionTitle => 'Start this session first';
|
||||
|
||||
@override
|
||||
String get reflectOneMinute => 'Write progress note';
|
||||
|
||||
@override
|
||||
String get startFocus => 'Start focus';
|
||||
|
||||
@override
|
||||
String get startAnotherSession => 'Start another session';
|
||||
@@ -94,6 +108,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get end => 'End';
|
||||
|
||||
@override
|
||||
String get completeSession => 'Complete session';
|
||||
|
||||
@override
|
||||
String get continueSession => 'Continue session';
|
||||
|
||||
@override
|
||||
String get resume => 'Resume';
|
||||
|
||||
@@ -123,7 +143,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get add => 'Add';
|
||||
|
||||
@override
|
||||
String get emptyHabitPrompt => 'Add one repeatable habit.';
|
||||
String get emptyHabitPrompt => 'Keep this optional and small.';
|
||||
|
||||
@override
|
||||
String viewAllHabits(int count) {
|
||||
@@ -167,7 +187,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String dailyStepsComplete(int completed) {
|
||||
return '$completed/4 daily steps complete';
|
||||
return '$completed/3 daily steps complete';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -180,13 +200,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get habitsTitle => 'Habits';
|
||||
String get habitsTitle => 'Supporting habits';
|
||||
|
||||
@override
|
||||
String get habitsSubtitle => 'Keep it small and repeatable.';
|
||||
String get habitsSubtitle => 'Optional repeatable steps. Today\'s focus still comes first.';
|
||||
|
||||
@override
|
||||
String get emptyHabitsTitle => 'Add a small habit';
|
||||
String get emptyHabitsTitle => 'Add a supporting habit';
|
||||
|
||||
@override
|
||||
String get addHabitTitle => 'Add habit';
|
||||
@@ -204,10 +224,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get save => 'Save';
|
||||
|
||||
@override
|
||||
String get setIntentionTitle => 'Set intention';
|
||||
String get textEntryRequired => 'Write a short line first.';
|
||||
|
||||
@override
|
||||
String get editIntentionTitle => 'Edit intention';
|
||||
String get setIntentionTitle => 'Set today\'s focus';
|
||||
|
||||
@override
|
||||
String get editIntentionTitle => 'Edit today\'s focus';
|
||||
|
||||
@override
|
||||
String get intentionHint => 'What matters most today?';
|
||||
@@ -249,7 +272,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
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 clear today\'s progress or delete all local data from Me at any time.';
|
||||
String get privacyBodyTwo => 'You can clear today\'s progress or delete all local data from settings at any time.';
|
||||
|
||||
@override
|
||||
String get reflectionEmptyError => 'Write one honest sentence.';
|
||||
@@ -258,19 +281,22 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get reflectionSaveError => 'Could not save. Please try again.';
|
||||
|
||||
@override
|
||||
String get reflectionSheetTitle => 'How did it go?';
|
||||
String get reflectionSheetTitle => 'Leave one progress note';
|
||||
|
||||
@override
|
||||
String get reflectionSheetSubtitle => 'Keep what worked. Adjust one thing.';
|
||||
String get reflectionSheetSubtitle => 'Save what moved forward so next time has a clear start.';
|
||||
|
||||
@override
|
||||
String get reflectionHint => 'One useful sentence is enough.';
|
||||
String get reflectionHint => 'What moved forward?';
|
||||
|
||||
@override
|
||||
String get reflectionMoodLabel => 'Mood is optional';
|
||||
|
||||
@override
|
||||
String get savingLabel => 'Saving';
|
||||
|
||||
@override
|
||||
String get saveReflection => 'Save reflection';
|
||||
String get saveReflection => 'Save note';
|
||||
|
||||
@override
|
||||
String get moodCalm => 'Calm';
|
||||
@@ -285,10 +311,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get moodTired => 'Tired';
|
||||
|
||||
@override
|
||||
String get meTitle => 'Your garden';
|
||||
String get meTitle => 'Me';
|
||||
|
||||
@override
|
||||
String get meSubtitle => 'Preferences and records stay on this device.';
|
||||
String get meSubtitle => 'Manage your rhythm, appearance, and records.';
|
||||
|
||||
@override
|
||||
String get appSettingsTitle => 'App settings';
|
||||
|
||||
@override
|
||||
String get appearance => 'Theme';
|
||||
@@ -321,7 +350,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get preferencesTitle => 'Preferences';
|
||||
|
||||
@override
|
||||
String get preferencesDetail => 'Make it feel right';
|
||||
String get preferencesDetail => 'Theme, language, and default focus';
|
||||
|
||||
@override
|
||||
String get defaultFocusDuration => 'Default focus';
|
||||
|
||||
@override
|
||||
String get defaultFocusDurationDetail => 'Next session';
|
||||
|
||||
@override
|
||||
String get localData => 'Local records';
|
||||
@@ -330,10 +365,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get localDataDetail => 'Private on device';
|
||||
|
||||
@override
|
||||
String get personalGardenTitle => 'Your focus garden';
|
||||
String get personalGardenTitle => 'My accumulation';
|
||||
|
||||
@override
|
||||
String get personalGardenSubtitle => 'Every small session leaves a trace you can return to.';
|
||||
String get personalGardenSubtitle => 'Your local action records, progress notes, and app controls stay on this device.';
|
||||
|
||||
@override
|
||||
String get focusDataLabel => 'Focus';
|
||||
@@ -342,7 +377,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get habitsDataLabel => 'Habits';
|
||||
|
||||
@override
|
||||
String get notesDataLabel => 'Notes';
|
||||
String get notesDataLabel => 'Progress';
|
||||
|
||||
@override
|
||||
String get totalActionTimeLabel => 'Action time';
|
||||
|
||||
@override
|
||||
String get recordsCountLabel => 'Records';
|
||||
|
||||
@override
|
||||
String get progressNotesCountLabel => 'Progress notes';
|
||||
|
||||
@override
|
||||
String get clearToday => 'Clear today';
|
||||
@@ -366,39 +410,33 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get localDataCleared => 'Local data deleted';
|
||||
|
||||
@override
|
||||
String get journeyTitle => 'Progress';
|
||||
String get todayLoopLabel => 'TODAY\'S LOOP';
|
||||
|
||||
@override
|
||||
String get journeyOverviewLabel => 'TODAY\'S LOOP';
|
||||
String get todayLoopStartTitle => 'Start with one clear focus';
|
||||
|
||||
@override
|
||||
String get journeyOverviewStartTitle => 'Start with one clear focus';
|
||||
String get todayLoopStartBody => 'Pick the one result that matters today.';
|
||||
|
||||
@override
|
||||
String get journeyOverviewStartBody => 'Pick the thing that would make today feel finished.';
|
||||
String get todayLoopActiveTitle => 'Your day is taking shape';
|
||||
|
||||
@override
|
||||
String get journeyOverviewActiveTitle => 'Your day is taking shape';
|
||||
String get todayLoopActiveBody => 'Focus once, then leave one useful note.';
|
||||
|
||||
@override
|
||||
String get journeyOverviewActiveBody => 'Keep the loop moving: focus, habits, then a short reflection.';
|
||||
String get todayLoopDoneTitle => 'Progress saved';
|
||||
|
||||
@override
|
||||
String get journeyOverviewDoneTitle => 'Loop complete';
|
||||
String get todayLoopDoneBody => 'You moved today forward and left a clear place to continue.';
|
||||
|
||||
@override
|
||||
String get journeyOverviewDoneBody => 'You left a clean record for future you.';
|
||||
|
||||
@override
|
||||
String journeyOverviewProgress(int completed) {
|
||||
return '$completed/4 done';
|
||||
String todayLoopProgress(int completed) {
|
||||
return '$completed/3 done';
|
||||
}
|
||||
|
||||
@override
|
||||
String get reviewNow => 'Reflect now';
|
||||
|
||||
@override
|
||||
String get journeyStatsTitle => '7-day rhythm';
|
||||
String get reviewNow => 'Write a note';
|
||||
|
||||
@override
|
||||
String get weekFocusLabel => 'Focus';
|
||||
@@ -441,13 +479,51 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get emptyFocusHistoryTitle => 'No focus sessions yet';
|
||||
|
||||
@override
|
||||
String get emptyFocusHistoryBody => 'Start one session from Today and it will appear here.';
|
||||
String get emptyFocusHistoryBody => 'Finish one focus session and it will appear here.';
|
||||
|
||||
@override
|
||||
String get emptyNotesHistoryTitle => 'No notes yet';
|
||||
|
||||
@override
|
||||
String get emptyNotesHistoryBody => 'Save a short reflection after focus to make your progress visible.';
|
||||
String get emptyNotesHistoryBody => 'Save one reflection after a session and it will appear here.';
|
||||
|
||||
@override
|
||||
String get recordsEmptyTitle => 'No progress records yet';
|
||||
|
||||
@override
|
||||
String get recordsEmptyBody => 'Finish one focus session and leave a note to keep today\'s progress here.';
|
||||
|
||||
@override
|
||||
String get recordsCalendarTitle => 'Activity dates';
|
||||
|
||||
@override
|
||||
String get recordsDateLedgerTitle => 'Date ledger';
|
||||
|
||||
@override
|
||||
String get backToToday => 'Back to Today';
|
||||
|
||||
@override
|
||||
String get recordsNoIntention => 'Unnamed daily focus';
|
||||
|
||||
@override
|
||||
String get recordsNoReflection => 'No progress note yet';
|
||||
|
||||
@override
|
||||
String get recordsMissingProgressHelper => 'Add one line so this is easier to pick up later.';
|
||||
|
||||
@override
|
||||
String get addProgressNote => 'Add progress note';
|
||||
|
||||
@override
|
||||
String get continueThisAction => 'Continue this';
|
||||
|
||||
@override
|
||||
String get replaceTodayActionTitle => 'Replace today\'s action?';
|
||||
|
||||
@override
|
||||
String replaceTodayActionBody(String actionTitle) {
|
||||
return 'This will change today\'s action to \"$actionTitle\". Existing records will stay.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recentFocus => 'Recent focus';
|
||||
@@ -485,57 +561,156 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get reflectionCaptured => 'Today\'s lesson is captured';
|
||||
|
||||
@override
|
||||
String get reflectionAction => 'Close the loop with one sentence';
|
||||
String get reflectionAction => 'Write one sentence to close today';
|
||||
|
||||
@override
|
||||
String get growthTitle => 'Growth';
|
||||
String get nextActionTitle => 'Next action';
|
||||
|
||||
@override
|
||||
String get growthFlowerOpen => 'Today\'s flower is open';
|
||||
String get nextActionSetFocusTitle => 'Write today\'s focus';
|
||||
|
||||
@override
|
||||
String get growthLocked => 'Unlocked after reflection';
|
||||
String get nextActionSetFocusBody => 'One short line is enough. You can adjust it later.';
|
||||
|
||||
@override
|
||||
String get sevenDayGrowth => '7-day growth';
|
||||
String get nextActionStartFocusTitle => 'Start 10 minutes';
|
||||
|
||||
@override
|
||||
String get sevenDayGrowthSubtitle => 'Small actions become visible over time.';
|
||||
String get nextActionStartFocusBody => 'Writing the action is optional. Start first.';
|
||||
|
||||
@override
|
||||
String get growthSeed => 'seed';
|
||||
String get nextActionStartActionBody => 'Move this forward in one small session, then leave one progress note.';
|
||||
|
||||
@override
|
||||
String get growthSprout => 'sprout';
|
||||
String get nextActionHabitsTitle => 'Finish one small habit';
|
||||
|
||||
@override
|
||||
String get growthLeaves => 'leaves';
|
||||
String get nextActionHabitsBody => 'Complete one repeatable step while it is still easy.';
|
||||
|
||||
@override
|
||||
String get growthFlower => 'flower';
|
||||
String get nextActionReflectTitle => 'Write a quick note';
|
||||
|
||||
@override
|
||||
String growthDaySemantic(Object date, Object stage) {
|
||||
return '$date, $stage';
|
||||
String get nextActionReflectBody => 'Record what moved forward so the next start is easier.';
|
||||
|
||||
@override
|
||||
String get nextActionCompleteTitle => 'Today moved forward';
|
||||
|
||||
@override
|
||||
String get nextActionCompleteBody => 'Your note is saved. Continue only if it still feels useful.';
|
||||
|
||||
@override
|
||||
String nextActionContinueFocus(int minutes) {
|
||||
return 'Focus $minutes min again';
|
||||
}
|
||||
|
||||
@override
|
||||
String get growthSeedSemantic => 'A seed waiting for today\'s intention';
|
||||
String get habitTemplatesTitle => 'Useful starts';
|
||||
|
||||
@override
|
||||
String get growthSproutSemantic => 'A sprout growing from today\'s intention';
|
||||
String get habitTemplatesBody => 'Add one tiny repeatable action.';
|
||||
|
||||
@override
|
||||
String get growthLeavesSemantic => 'A young plant growing with today\'s progress';
|
||||
String get habitTemplateStretch => '2 min stretch';
|
||||
|
||||
@override
|
||||
String get growthFlowerSemantic => 'A flower marking a complete daily cycle';
|
||||
String get habitTemplateRead => 'Read 10 pages';
|
||||
|
||||
@override
|
||||
String get habitTemplateWalk => 'Walk outside';
|
||||
|
||||
@override
|
||||
String get habitTemplateNoScroll => 'No-scroll lunch';
|
||||
|
||||
@override
|
||||
String get languageDetail => 'Interface language';
|
||||
|
||||
@override
|
||||
String get recordsAndPrivacyTitle => 'Records & privacy';
|
||||
|
||||
@override
|
||||
String get recordsAndPrivacyBody => 'Your history stays local. Keep, reset, or delete it when needed.';
|
||||
|
||||
@override
|
||||
String get welcomeTrustLabel => 'TODAY';
|
||||
|
||||
@override
|
||||
String get welcomeTitle => 'Plant just one thing each day.';
|
||||
|
||||
@override
|
||||
String get welcomeBody => 'Start with today\'s most important thing.';
|
||||
|
||||
@override
|
||||
String get welcomeNext => 'The next step starts here.';
|
||||
|
||||
@override
|
||||
String get onboardingFocusBadge => 'FOCUS';
|
||||
|
||||
@override
|
||||
String get onboardingFocusTitle => 'Protect 10 minutes first.';
|
||||
|
||||
@override
|
||||
String get onboardingFocusBody => 'It does not have to be long. Starting creates progress.';
|
||||
|
||||
@override
|
||||
String get onboardingFocusNext => 'Keep this one thing in front of you while you focus.';
|
||||
|
||||
@override
|
||||
String get onboardingReflectBadge => 'NOTE';
|
||||
|
||||
@override
|
||||
String get onboardingReflectTitle => 'Leave one progress note.';
|
||||
|
||||
@override
|
||||
String get onboardingReflectBody => 'When you finish, write one line so you know where to continue next time.';
|
||||
|
||||
@override
|
||||
String get onboardingReflectNext => 'Now set today\'s focus.';
|
||||
|
||||
@override
|
||||
String get onboardingNext => 'Continue';
|
||||
|
||||
@override
|
||||
String get welcomeStart => 'Set today\'s focus';
|
||||
|
||||
@override
|
||||
String get welcomeSkip => 'Skip';
|
||||
|
||||
@override
|
||||
String get landingTitle => 'Start with 10 minutes.';
|
||||
|
||||
@override
|
||||
String get landingBody => 'Writing is optional. Start first.';
|
||||
|
||||
@override
|
||||
String get landingFocusLabel => 'What do you want to do first?';
|
||||
|
||||
@override
|
||||
String get landingFocusEntry => 'Write down what you will do';
|
||||
|
||||
@override
|
||||
String get landingFocusHint => 'Example: finish the launch checklist';
|
||||
|
||||
@override
|
||||
String get customFocusDuration => 'Custom length';
|
||||
|
||||
@override
|
||||
String get customFocusDurationHint => 'Enter 1-180 minutes';
|
||||
|
||||
@override
|
||||
String get minutesInputLabel => 'Minutes';
|
||||
|
||||
@override
|
||||
String get landingStart => 'Start 10 minutes';
|
||||
|
||||
@override
|
||||
String get landingSkip => 'Enter Today';
|
||||
|
||||
@override
|
||||
String get defaultHabitPlanTask => 'Plan one clear task';
|
||||
|
||||
@override
|
||||
String get defaultHabitFocus => 'Focus for 25 minutes';
|
||||
String get defaultHabitFocus => 'Focus for 10 minutes';
|
||||
|
||||
@override
|
||||
String get defaultHabitReflect => 'Write one-line reflection';
|
||||
|
||||
@@ -24,7 +24,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get navToday => '今日';
|
||||
|
||||
@override
|
||||
String get navJourney => '进展';
|
||||
String get navRecords => '记录';
|
||||
|
||||
@override
|
||||
String get navMe => '我的';
|
||||
@@ -35,6 +35,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get couldNotOpenApp => '无法打开 Focus Seed。';
|
||||
|
||||
@override
|
||||
String get loadingLocalData => '正在读取本地数据';
|
||||
|
||||
@override
|
||||
String get openFailed => '本地数据打开失败。';
|
||||
|
||||
@@ -80,7 +83,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get timerCompleted => '已完成';
|
||||
|
||||
@override
|
||||
String get reflectOneMinute => '复盘 1 分钟';
|
||||
String focusCurrentIntention(String intention) {
|
||||
return '今日重点:$intention';
|
||||
}
|
||||
|
||||
@override
|
||||
String get focusNoIntentionTitle => '先开始这一段';
|
||||
|
||||
@override
|
||||
String get reflectOneMinute => '写一句进展';
|
||||
|
||||
@override
|
||||
String get startFocus => '开始专注';
|
||||
|
||||
@override
|
||||
String get startAnotherSession => '再来一轮';
|
||||
@@ -94,6 +108,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get end => '结束';
|
||||
|
||||
@override
|
||||
String get completeSession => '完成本轮';
|
||||
|
||||
@override
|
||||
String get continueSession => '继续本轮';
|
||||
|
||||
@override
|
||||
String get resume => '继续';
|
||||
|
||||
@@ -123,7 +143,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get add => '添加';
|
||||
|
||||
@override
|
||||
String get emptyHabitPrompt => '添加一个可重复的小习惯。';
|
||||
String get emptyHabitPrompt => '保持可选,也保持很小。';
|
||||
|
||||
@override
|
||||
String viewAllHabits(int count) {
|
||||
@@ -167,7 +187,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String dailyStepsComplete(int completed) {
|
||||
return '今日完成 $completed/4';
|
||||
return '今日完成 $completed/3';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -180,13 +200,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get delete => '删除';
|
||||
|
||||
@override
|
||||
String get habitsTitle => '习惯';
|
||||
String get habitsTitle => '辅助习惯';
|
||||
|
||||
@override
|
||||
String get habitsSubtitle => '少量、可重复。';
|
||||
String get habitsSubtitle => '可选的重复动作,今日重点仍然放在前面。';
|
||||
|
||||
@override
|
||||
String get emptyHabitsTitle => '添加小习惯';
|
||||
String get emptyHabitsTitle => '添加一个辅助习惯';
|
||||
|
||||
@override
|
||||
String get addHabitTitle => '添加习惯';
|
||||
@@ -204,10 +224,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get save => '保存';
|
||||
|
||||
@override
|
||||
String get setIntentionTitle => '设置目标';
|
||||
String get textEntryRequired => '先写一个短句。';
|
||||
|
||||
@override
|
||||
String get editIntentionTitle => '编辑目标';
|
||||
String get setIntentionTitle => '设置今日重点';
|
||||
|
||||
@override
|
||||
String get editIntentionTitle => '编辑今日重点';
|
||||
|
||||
@override
|
||||
String get intentionHint => '今天最重要的是什么?';
|
||||
@@ -249,7 +272,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get privacyBodyOne => 'Focus Seed 只在本机保存目标、习惯、专注记录、复盘和设置。当前版本没有登录、云同步、广告、分析 SDK 或付费 SDK。';
|
||||
|
||||
@override
|
||||
String get privacyBodyTwo => '你可以随时清理今日进度,或删除本机的全部本地数据。';
|
||||
String get privacyBodyTwo => '你可以随时在设置中清理今日进度,或删除本机的全部本地数据。';
|
||||
|
||||
@override
|
||||
String get reflectionEmptyError => '写一句真实的总结。';
|
||||
@@ -258,19 +281,22 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get reflectionSaveError => '保存失败,请重试。';
|
||||
|
||||
@override
|
||||
String get reflectionSheetTitle => '复盘一下';
|
||||
String get reflectionSheetTitle => '留下一句进展';
|
||||
|
||||
@override
|
||||
String get reflectionSheetSubtitle => '保留有效的,调整一个点。';
|
||||
String get reflectionSheetSubtitle => '记下刚推进了什么,下次就知道从哪里继续。';
|
||||
|
||||
@override
|
||||
String get reflectionHint => '一句有用的话就够。';
|
||||
String get reflectionHint => '刚才推进了什么?';
|
||||
|
||||
@override
|
||||
String get reflectionMoodLabel => '状态可以稍后补';
|
||||
|
||||
@override
|
||||
String get savingLabel => '保存中';
|
||||
|
||||
@override
|
||||
String get saveReflection => '保存复盘';
|
||||
String get saveReflection => '保存记录';
|
||||
|
||||
@override
|
||||
String get moodCalm => '平静';
|
||||
@@ -285,10 +311,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get moodTired => '疲惫';
|
||||
|
||||
@override
|
||||
String get meTitle => '我的花园';
|
||||
String get meTitle => '我的';
|
||||
|
||||
@override
|
||||
String get meSubtitle => '偏好和记录都只保存在本机。';
|
||||
String get meSubtitle => '管理你的节奏、外观和记录。';
|
||||
|
||||
@override
|
||||
String get appSettingsTitle => '应用设置';
|
||||
|
||||
@override
|
||||
String get appearance => '主题';
|
||||
@@ -321,7 +350,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get preferencesTitle => '偏好';
|
||||
|
||||
@override
|
||||
String get preferencesDetail => '让界面更顺手';
|
||||
String get preferencesDetail => '主题、语言和默认专注';
|
||||
|
||||
@override
|
||||
String get defaultFocusDuration => '默认专注';
|
||||
|
||||
@override
|
||||
String get defaultFocusDurationDetail => '下一轮';
|
||||
|
||||
@override
|
||||
String get localData => '本地记录';
|
||||
@@ -330,10 +365,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get localDataDetail => '只在本机';
|
||||
|
||||
@override
|
||||
String get personalGardenTitle => '你的专注花园';
|
||||
String get personalGardenTitle => '我的累计';
|
||||
|
||||
@override
|
||||
String get personalGardenSubtitle => '每一次小专注,都会留下可回看的痕迹。';
|
||||
String get personalGardenSubtitle => '你的本地行动记录、进展和应用控制都保留在这台设备上。';
|
||||
|
||||
@override
|
||||
String get focusDataLabel => '专注';
|
||||
@@ -342,7 +377,16 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get habitsDataLabel => '习惯';
|
||||
|
||||
@override
|
||||
String get notesDataLabel => '记录';
|
||||
String get notesDataLabel => '进展';
|
||||
|
||||
@override
|
||||
String get totalActionTimeLabel => '行动时间';
|
||||
|
||||
@override
|
||||
String get recordsCountLabel => '记录';
|
||||
|
||||
@override
|
||||
String get progressNotesCountLabel => '进展';
|
||||
|
||||
@override
|
||||
String get clearToday => '清理今天';
|
||||
@@ -366,39 +410,33 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get localDataCleared => '本地数据已删除';
|
||||
|
||||
@override
|
||||
String get journeyTitle => '进展';
|
||||
String get todayLoopLabel => '今日闭环';
|
||||
|
||||
@override
|
||||
String get journeyOverviewLabel => '今日闭环';
|
||||
String get todayLoopStartTitle => '先定下一个重点';
|
||||
|
||||
@override
|
||||
String get journeyOverviewStartTitle => '先定下一个重点';
|
||||
String get todayLoopStartBody => '先写下今天最重要的一件事。';
|
||||
|
||||
@override
|
||||
String get journeyOverviewStartBody => '写下今天最值得完成的那件事。';
|
||||
String get todayLoopActiveTitle => '今天正在成形';
|
||||
|
||||
@override
|
||||
String get journeyOverviewActiveTitle => '今天正在成形';
|
||||
String get todayLoopActiveBody => '先完成一轮专注,再留一句有用的记录。';
|
||||
|
||||
@override
|
||||
String get journeyOverviewActiveBody => '继续推进:专注、打卡,然后用一句话复盘。';
|
||||
String get todayLoopDoneTitle => '进展已保存';
|
||||
|
||||
@override
|
||||
String get journeyOverviewDoneTitle => '今日闭环完成';
|
||||
String get todayLoopDoneBody => '今天已经推进了一点,下次也知道从哪里继续。';
|
||||
|
||||
@override
|
||||
String get journeyOverviewDoneBody => '你已经给未来的自己留下了一条清晰记录。';
|
||||
|
||||
@override
|
||||
String journeyOverviewProgress(int completed) {
|
||||
return '已完成 $completed/4';
|
||||
String todayLoopProgress(int completed) {
|
||||
return '已完成 $completed/3';
|
||||
}
|
||||
|
||||
@override
|
||||
String get reviewNow => '现在复盘';
|
||||
|
||||
@override
|
||||
String get journeyStatsTitle => '7 日节奏';
|
||||
String get reviewNow => '写一句记录';
|
||||
|
||||
@override
|
||||
String get weekFocusLabel => '专注';
|
||||
@@ -441,13 +479,51 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get emptyFocusHistoryTitle => '还没有专注记录';
|
||||
|
||||
@override
|
||||
String get emptyFocusHistoryBody => '从今日页开始一轮专注,完成后会出现在这里。';
|
||||
String get emptyFocusHistoryBody => '完成一轮专注后,记录会出现在这里。';
|
||||
|
||||
@override
|
||||
String get emptyNotesHistoryTitle => '还没有复盘';
|
||||
|
||||
@override
|
||||
String get emptyNotesHistoryBody => '专注后保存一句复盘,让进展更清晰。';
|
||||
String get emptyNotesHistoryBody => '专注后留一句复盘,这里会保留它。';
|
||||
|
||||
@override
|
||||
String get recordsEmptyTitle => '还没有进展记录';
|
||||
|
||||
@override
|
||||
String get recordsEmptyBody => '完成一次专注,再留一句记录,这里就会保留今天的进展。';
|
||||
|
||||
@override
|
||||
String get recordsCalendarTitle => '活动日期';
|
||||
|
||||
@override
|
||||
String get recordsDateLedgerTitle => '当日账本';
|
||||
|
||||
@override
|
||||
String get backToToday => '回到今日';
|
||||
|
||||
@override
|
||||
String get recordsNoIntention => '未命名今日重点';
|
||||
|
||||
@override
|
||||
String get recordsNoReflection => '还没有进展记录';
|
||||
|
||||
@override
|
||||
String get recordsMissingProgressHelper => '补一句,之后更容易接上。';
|
||||
|
||||
@override
|
||||
String get addProgressNote => '补一句进展';
|
||||
|
||||
@override
|
||||
String get continueThisAction => '继续这件事';
|
||||
|
||||
@override
|
||||
String get replaceTodayActionTitle => '替换今天的行动?';
|
||||
|
||||
@override
|
||||
String replaceTodayActionBody(String actionTitle) {
|
||||
return '会把今天的行动改为“$actionTitle”,已有记录不会删除。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recentFocus => '最近专注';
|
||||
@@ -485,57 +561,156 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get reflectionCaptured => '今日复盘已记录';
|
||||
|
||||
@override
|
||||
String get reflectionAction => '用一句话收尾';
|
||||
String get reflectionAction => '用一句话给今天收尾';
|
||||
|
||||
@override
|
||||
String get growthTitle => '成长';
|
||||
String get nextActionTitle => '下一步';
|
||||
|
||||
@override
|
||||
String get growthFlowerOpen => '今日花朵已开放';
|
||||
String get nextActionSetFocusTitle => '写下今天的重点';
|
||||
|
||||
@override
|
||||
String get growthLocked => '复盘后解锁';
|
||||
String get nextActionSetFocusBody => '一个短句就够了,之后还可以调整。';
|
||||
|
||||
@override
|
||||
String get sevenDayGrowth => '7 日成长';
|
||||
String get nextActionStartFocusTitle => '开始 10 分钟';
|
||||
|
||||
@override
|
||||
String get sevenDayGrowthSubtitle => '连续行动会被看见。';
|
||||
String get nextActionStartFocusBody => '写不写都可以,先开始。';
|
||||
|
||||
@override
|
||||
String get growthSeed => '种子';
|
||||
String get nextActionStartActionBody => '先推进一小段,完成后留一句进展。';
|
||||
|
||||
@override
|
||||
String get growthSprout => '发芽';
|
||||
String get nextActionHabitsTitle => '完成一个小习惯';
|
||||
|
||||
@override
|
||||
String get growthLeaves => '生长';
|
||||
String get nextActionHabitsBody => '趁着还顺手,先做掉一个可重复的小动作。';
|
||||
|
||||
@override
|
||||
String get growthFlower => '开花';
|
||||
String get nextActionReflectTitle => '写一句记录';
|
||||
|
||||
@override
|
||||
String growthDaySemantic(Object date, Object stage) {
|
||||
return '$date,$stage';
|
||||
String get nextActionReflectBody => '记下刚才推进了什么,下次更容易继续。';
|
||||
|
||||
@override
|
||||
String get nextActionCompleteTitle => '今天已经推进了一点';
|
||||
|
||||
@override
|
||||
String get nextActionCompleteBody => '进展已经留下,想继续时再来一段。';
|
||||
|
||||
@override
|
||||
String nextActionContinueFocus(int minutes) {
|
||||
return '再专注 $minutes 分钟';
|
||||
}
|
||||
|
||||
@override
|
||||
String get growthSeedSemantic => '等待今日目标的种子';
|
||||
String get habitTemplatesTitle => '可以直接开始';
|
||||
|
||||
@override
|
||||
String get growthSproutSemantic => '目标已种下,正在发芽';
|
||||
String get habitTemplatesBody => '添加一个很小但能重复的动作。';
|
||||
|
||||
@override
|
||||
String get growthLeavesSemantic => '今日进度让植物生长';
|
||||
String get habitTemplateStretch => '2 分钟拉伸';
|
||||
|
||||
@override
|
||||
String get growthFlowerSemantic => '今日闭环完成,花朵开放';
|
||||
String get habitTemplateRead => '读 10 页书';
|
||||
|
||||
@override
|
||||
String get habitTemplateWalk => '出门走走';
|
||||
|
||||
@override
|
||||
String get habitTemplateNoScroll => '午餐不刷手机';
|
||||
|
||||
@override
|
||||
String get languageDetail => '界面语言';
|
||||
|
||||
@override
|
||||
String get recordsAndPrivacyTitle => '记录与隐私';
|
||||
|
||||
@override
|
||||
String get recordsAndPrivacyBody => '历史只保存在本机。你可以保留、重置或彻底删除。';
|
||||
|
||||
@override
|
||||
String get welcomeTrustLabel => '今日';
|
||||
|
||||
@override
|
||||
String get welcomeTitle => '每天只种一件事。';
|
||||
|
||||
@override
|
||||
String get welcomeBody => '先选今天最重要的一件。';
|
||||
|
||||
@override
|
||||
String get welcomeNext => '下一步会从这里开始。';
|
||||
|
||||
@override
|
||||
String get onboardingFocusBadge => '专注';
|
||||
|
||||
@override
|
||||
String get onboardingFocusTitle => '先守住 10 分钟。';
|
||||
|
||||
@override
|
||||
String get onboardingFocusBody => '时间不用长,能开始就有进展。';
|
||||
|
||||
@override
|
||||
String get onboardingFocusNext => '专注时,把这件事留在眼前。';
|
||||
|
||||
@override
|
||||
String get onboardingReflectBadge => '记录';
|
||||
|
||||
@override
|
||||
String get onboardingReflectTitle => '留下一句进展。';
|
||||
|
||||
@override
|
||||
String get onboardingReflectBody => '结束时记一句,下次就知道从哪里继续。';
|
||||
|
||||
@override
|
||||
String get onboardingReflectNext => '现在设置今天重点。';
|
||||
|
||||
@override
|
||||
String get onboardingNext => '继续';
|
||||
|
||||
@override
|
||||
String get welcomeStart => '设置今天重点';
|
||||
|
||||
@override
|
||||
String get welcomeSkip => '跳过';
|
||||
|
||||
@override
|
||||
String get landingTitle => '先开始 10 分钟。';
|
||||
|
||||
@override
|
||||
String get landingBody => '写不写都可以,先开始。';
|
||||
|
||||
@override
|
||||
String get landingFocusLabel => '今天想先做什么?';
|
||||
|
||||
@override
|
||||
String get landingFocusEntry => '写下这次要做的事';
|
||||
|
||||
@override
|
||||
String get landingFocusHint => '例如:完成上线检查清单';
|
||||
|
||||
@override
|
||||
String get customFocusDuration => '自定义时长';
|
||||
|
||||
@override
|
||||
String get customFocusDurationHint => '输入 1-180 分钟';
|
||||
|
||||
@override
|
||||
String get minutesInputLabel => '分钟数';
|
||||
|
||||
@override
|
||||
String get landingStart => '开始 10 分钟';
|
||||
|
||||
@override
|
||||
String get landingSkip => '进入今日页';
|
||||
|
||||
@override
|
||||
String get defaultHabitPlanTask => '计划一个明确任务';
|
||||
|
||||
@override
|
||||
String get defaultHabitFocus => '专注 25 分钟';
|
||||
String get defaultHabitFocus => '专注 10 分钟';
|
||||
|
||||
@override
|
||||
String get defaultHabitReflect => '写一句复盘';
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../core/ambience.dart';
|
||||
import '../models/growth_stage.dart';
|
||||
import '../models/mood.dart';
|
||||
import 'generated/app_localizations.dart';
|
||||
|
||||
@@ -46,24 +45,6 @@ String localizedMood(AppLocalizations l10n, Mood mood) {
|
||||
};
|
||||
}
|
||||
|
||||
String localizedGrowthStage(AppLocalizations l10n, GrowthStage stage) {
|
||||
return switch (stage) {
|
||||
GrowthStage.seed => l10n.growthSeed,
|
||||
GrowthStage.sprout => l10n.growthSprout,
|
||||
GrowthStage.leaves => l10n.growthLeaves,
|
||||
GrowthStage.flower => l10n.growthFlower,
|
||||
};
|
||||
}
|
||||
|
||||
String localizedGrowthSemantic(AppLocalizations l10n, GrowthStage stage) {
|
||||
return switch (stage) {
|
||||
GrowthStage.seed => l10n.growthSeedSemantic,
|
||||
GrowthStage.sprout => l10n.growthSproutSemantic,
|
||||
GrowthStage.leaves => l10n.growthLeavesSemantic,
|
||||
GrowthStage.flower => l10n.growthFlowerSemantic,
|
||||
};
|
||||
}
|
||||
|
||||
List<String> localizedStarterHabits(AppLocalizations l10n) {
|
||||
return [
|
||||
l10n.defaultHabitPlanTask,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import '../core/focus_durations.dart';
|
||||
import 'daily_action.dart';
|
||||
import 'focus_session.dart';
|
||||
import 'habit.dart';
|
||||
import 'progress_note.dart';
|
||||
import 'reflection_entry.dart';
|
||||
|
||||
class AppState {
|
||||
@@ -9,6 +12,11 @@ class AppState {
|
||||
required this.habits,
|
||||
required this.sessions,
|
||||
required this.reflections,
|
||||
this.actions = const [],
|
||||
this.selectedActionId = '',
|
||||
this.progressNotes = const [],
|
||||
this.recentLabels = const [],
|
||||
this.defaultFocusMinutes = defaultFocusDurationMinutes,
|
||||
});
|
||||
|
||||
final String anchor;
|
||||
@@ -16,23 +24,101 @@ class AppState {
|
||||
final List<Habit> habits;
|
||||
final List<FocusSession> sessions;
|
||||
final List<ReflectionEntry> reflections;
|
||||
final List<DailyAction> actions;
|
||||
final String selectedActionId;
|
||||
final List<ProgressNote> progressNotes;
|
||||
final List<String> recentLabels;
|
||||
final int defaultFocusMinutes;
|
||||
|
||||
bool get isFreshStart =>
|
||||
habits.isEmpty &&
|
||||
sessions.isEmpty &&
|
||||
reflections.isEmpty &&
|
||||
anchor.isEmpty;
|
||||
anchor.isEmpty &&
|
||||
actions.isEmpty &&
|
||||
progressNotes.isEmpty;
|
||||
|
||||
String activeAnchor(String dateKey) {
|
||||
return anchorDateKey == dateKey ? anchor : '';
|
||||
}
|
||||
|
||||
List<DailyAction> actionsForDate(
|
||||
String dateKey, {
|
||||
bool includeArchived = false,
|
||||
}) {
|
||||
final filtered = actions
|
||||
.where(
|
||||
(action) =>
|
||||
action.dateKey == dateKey &&
|
||||
(includeArchived || !action.isArchived),
|
||||
)
|
||||
.toList(growable: false);
|
||||
return [...filtered]..sort((a, b) {
|
||||
final byOrder = a.orderIndex.compareTo(b.orderIndex);
|
||||
if (byOrder != 0) {
|
||||
return byOrder;
|
||||
}
|
||||
return a.createdAt.compareTo(b.createdAt);
|
||||
});
|
||||
}
|
||||
|
||||
DailyAction? actionById(String? id) {
|
||||
if (id == null || id.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final action in actions) {
|
||||
if (action.id == id) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DailyAction? selectedAction(String dateKey) {
|
||||
final todayActions = actionsForDate(dateKey);
|
||||
if (todayActions.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final selected = actionById(selectedActionId);
|
||||
if (selected != null &&
|
||||
selected.dateKey == dateKey &&
|
||||
!selected.isArchived) {
|
||||
return selected;
|
||||
}
|
||||
for (final action in todayActions) {
|
||||
if (action.status == DailyActionStatus.active ||
|
||||
action.status == DailyActionStatus.focused) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return todayActions.first;
|
||||
}
|
||||
|
||||
List<FocusSession> sessionsForAction(String actionId) {
|
||||
return sessions
|
||||
.where((session) => session.actionId == actionId)
|
||||
.toList(growable: false)
|
||||
..sort((a, b) => b.completedAt.compareTo(a.completedAt));
|
||||
}
|
||||
|
||||
List<ProgressNote> progressNotesForAction(String actionId) {
|
||||
return progressNotes
|
||||
.where((note) => note.actionId == actionId)
|
||||
.toList(growable: false)
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
AppState copyWith({
|
||||
String? anchor,
|
||||
String? anchorDateKey,
|
||||
List<Habit>? habits,
|
||||
List<FocusSession>? sessions,
|
||||
List<ReflectionEntry>? reflections,
|
||||
List<DailyAction>? actions,
|
||||
String? selectedActionId,
|
||||
List<ProgressNote>? progressNotes,
|
||||
List<String>? recentLabels,
|
||||
int? defaultFocusMinutes,
|
||||
}) {
|
||||
return AppState(
|
||||
anchor: anchor ?? this.anchor,
|
||||
@@ -40,6 +126,11 @@ class AppState {
|
||||
habits: habits ?? this.habits,
|
||||
sessions: sessions ?? this.sessions,
|
||||
reflections: reflections ?? this.reflections,
|
||||
actions: actions ?? this.actions,
|
||||
selectedActionId: selectedActionId ?? this.selectedActionId,
|
||||
progressNotes: progressNotes ?? this.progressNotes,
|
||||
recentLabels: recentLabels ?? this.recentLabels,
|
||||
defaultFocusMinutes: defaultFocusMinutes ?? this.defaultFocusMinutes,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,11 +143,19 @@ class AppState {
|
||||
'sessions': sessions.map((session) => session.toJson()).toList(),
|
||||
'reflections':
|
||||
reflections.map((reflection) => reflection.toJson()).toList(),
|
||||
'actions': actions.map((action) => action.toJson()).toList(),
|
||||
'selectedActionId': selectedActionId,
|
||||
'progressNotes': progressNotes.map((note) => note.toJson()).toList(),
|
||||
'recentLabels': recentLabels,
|
||||
'defaultFocusMinutes': defaultFocusMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
static AppState fromJson(Map<String, dynamic> json) {
|
||||
final anchor = (json['anchor'] as String?) ?? '';
|
||||
final defaultMinutes = _validDefaultFocusMinutes(
|
||||
json['defaultFocusMinutes'] as int?,
|
||||
);
|
||||
return AppState(
|
||||
anchor: anchor,
|
||||
anchorDateKey:
|
||||
@@ -69,6 +168,14 @@ class AppState {
|
||||
_jsonObjects(
|
||||
json['reflections'],
|
||||
).map(ReflectionEntry.fromJson).toList(),
|
||||
actions: _jsonObjects(json['actions']).map(DailyAction.fromJson).toList(),
|
||||
selectedActionId: (json['selectedActionId'] as String?) ?? '',
|
||||
progressNotes:
|
||||
_jsonObjects(
|
||||
json['progressNotes'],
|
||||
).map(ProgressNote.fromJson).toList(),
|
||||
recentLabels: _stringList(json['recentLabels']),
|
||||
defaultFocusMinutes: defaultMinutes,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,6 +186,16 @@ class AppState {
|
||||
return value.whereType<Map<String, dynamic>>();
|
||||
}
|
||||
|
||||
static List<String> _stringList(Object? value) {
|
||||
if (value is! List<Object?>) {
|
||||
return const [];
|
||||
}
|
||||
return [
|
||||
for (final item in value)
|
||||
if (item is String && item.trim().isNotEmpty) item.trim(),
|
||||
];
|
||||
}
|
||||
|
||||
static AppState starter({List<String>? defaultHabitTitles}) {
|
||||
final now = DateTime.now();
|
||||
final titles =
|
||||
@@ -86,7 +203,7 @@ class AppState {
|
||||
? defaultHabitTitles!
|
||||
: const [
|
||||
'Plan one clear task',
|
||||
'Focus for 25 minutes',
|
||||
'Focus for 10 minutes',
|
||||
'Write one-line reflection',
|
||||
];
|
||||
return AppState(
|
||||
@@ -117,6 +234,13 @@ class AppState {
|
||||
);
|
||||
}
|
||||
|
||||
static int _validDefaultFocusMinutes(int? value) {
|
||||
if (value == null || !isFocusDurationPreset(value)) {
|
||||
return defaultFocusDurationMinutes;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static String _currentDateKey([DateTime? value]) {
|
||||
final date = value ?? DateTime.now();
|
||||
return '${date.year.toString().padLeft(4, '0')}-'
|
||||
|
||||
@@ -3,25 +3,49 @@ class FocusSession {
|
||||
required this.id,
|
||||
required this.minutes,
|
||||
required this.completedAt,
|
||||
});
|
||||
this.intention = '',
|
||||
this.actionId,
|
||||
String? actionTitleSnapshot,
|
||||
int? actualSeconds,
|
||||
}) : actualSeconds = actualSeconds ?? minutes * 60,
|
||||
actionTitleSnapshot = actionTitleSnapshot ?? intention;
|
||||
|
||||
final String id;
|
||||
final int minutes;
|
||||
final int actualSeconds;
|
||||
final DateTime completedAt;
|
||||
final String intention;
|
||||
final String? actionId;
|
||||
final String actionTitleSnapshot;
|
||||
|
||||
int get actualMinutes => actualSeconds <= 0 ? 0 : (actualSeconds + 59) ~/ 60;
|
||||
String get actionTitle =>
|
||||
actionTitleSnapshot.trim().isEmpty
|
||||
? intention.trim()
|
||||
: actionTitleSnapshot.trim();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'minutes': minutes,
|
||||
'actualSeconds': actualSeconds,
|
||||
'completedAt': completedAt.toIso8601String(),
|
||||
'intention': intention,
|
||||
'actionId': actionId,
|
||||
'actionTitleSnapshot': actionTitleSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
static FocusSession fromJson(Map<String, dynamic> json) {
|
||||
final minutes = json['minutes'] as int;
|
||||
return FocusSession(
|
||||
id: json['id'] as String,
|
||||
minutes: json['minutes'] as int,
|
||||
minutes: minutes,
|
||||
actualSeconds: (json['actualSeconds'] as int?) ?? minutes * 60,
|
||||
completedAt: DateTime.parse(json['completedAt'] as String),
|
||||
intention: (json['intention'] as String?) ?? '',
|
||||
actionId: json['actionId'] as String?,
|
||||
actionTitleSnapshot: json['actionTitleSnapshot'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import '../core/date_keys.dart';
|
||||
import 'app_state.dart';
|
||||
|
||||
enum GrowthStage { seed, sprout, leaves, flower }
|
||||
|
||||
GrowthStage growthStageFor(AppState state, String dateKey) {
|
||||
if (state.reflections.any((entry) => entry.dateKey == dateKey)) {
|
||||
return GrowthStage.flower;
|
||||
}
|
||||
final hasFocus = minutesForDate(state.sessions, dateKey) > 0;
|
||||
final hasHabit = state.habits.any(
|
||||
(habit) => habit.completedDates.contains(dateKey),
|
||||
);
|
||||
if (hasFocus || hasHabit) {
|
||||
return GrowthStage.leaves;
|
||||
}
|
||||
return state.activeAnchor(dateKey).isEmpty
|
||||
? GrowthStage.seed
|
||||
: GrowthStage.sprout;
|
||||
}
|
||||
|
||||
extension GrowthStagePresentation on GrowthStage {
|
||||
String get assetPath => switch (this) {
|
||||
GrowthStage.seed => 'assets/images/growth/growth-seed.png',
|
||||
GrowthStage.sprout => 'assets/images/growth/growth-sprout.png',
|
||||
GrowthStage.leaves => 'assets/images/growth/growth-leaves.png',
|
||||
GrowthStage.flower => 'assets/images/growth/growth-flower.png',
|
||||
};
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import '../core/date_keys.dart';
|
||||
import 'app_state.dart';
|
||||
|
||||
class JourneyStats {
|
||||
const JourneyStats({
|
||||
required this.currentStreakDays,
|
||||
required this.weekFocusMinutes,
|
||||
required this.activeDaysThisWeek,
|
||||
required this.habitCompletionRate,
|
||||
});
|
||||
|
||||
final int currentStreakDays;
|
||||
final int weekFocusMinutes;
|
||||
final int activeDaysThisWeek;
|
||||
final double habitCompletionRate;
|
||||
|
||||
static JourneyStats fromState(AppState state) {
|
||||
final dateKeys = lastSevenDateKeys();
|
||||
final dateKeySet = dateKeys.toSet();
|
||||
final weekFocusMinutes = dateKeys.fold<int>(
|
||||
0,
|
||||
(total, key) => total + minutesForDate(state.sessions, key),
|
||||
);
|
||||
final activeDays = dateKeys.where((key) => _hasProgress(state, key)).length;
|
||||
final habitCompletions = state.habits.fold<int>(
|
||||
0,
|
||||
(total, habit) =>
|
||||
total + habit.completedDates.where(dateKeySet.contains).length,
|
||||
);
|
||||
final habitOpportunities = state.habits.length * dateKeys.length;
|
||||
|
||||
return JourneyStats(
|
||||
currentStreakDays: currentStreak(
|
||||
state.habits,
|
||||
state.sessions,
|
||||
state.reflections,
|
||||
),
|
||||
weekFocusMinutes: weekFocusMinutes,
|
||||
activeDaysThisWeek: activeDays,
|
||||
habitCompletionRate:
|
||||
habitOpportunities == 0 ? 0 : habitCompletions / habitOpportunities,
|
||||
);
|
||||
}
|
||||
|
||||
static bool _hasProgress(AppState state, String key) {
|
||||
return state.habits.any((habit) => habit.completedDates.contains(key)) ||
|
||||
state.sessions.any((session) => dateKey(session.completedAt) == key) ||
|
||||
state.reflections.any((reflection) => reflection.dateKey == key);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import '../core/date_keys.dart';
|
||||
import 'app_state.dart';
|
||||
import 'growth_stage.dart';
|
||||
|
||||
class TodayProgress {
|
||||
const TodayProgress({
|
||||
@@ -10,7 +9,6 @@ class TodayProgress {
|
||||
required this.completedHabits,
|
||||
required this.totalHabits,
|
||||
required this.hasReflection,
|
||||
required this.growthStage,
|
||||
});
|
||||
|
||||
factory TodayProgress.fromState(AppState state, String dateKey) {
|
||||
@@ -23,8 +21,11 @@ class TodayProgress {
|
||||
.where((habit) => habit.completedDates.contains(dateKey))
|
||||
.length,
|
||||
totalHabits: state.habits.length,
|
||||
hasReflection: state.reflections.any((entry) => entry.dateKey == dateKey),
|
||||
growthStage: growthStageFor(state, dateKey),
|
||||
hasReflection:
|
||||
state.reflections.any((entry) => entry.dateKey == dateKey) ||
|
||||
state.progressNotes.any(
|
||||
(entry) => entry.dateKey == dateKey && entry.hasContent,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,16 +35,15 @@ class TodayProgress {
|
||||
final int completedHabits;
|
||||
final int totalHabits;
|
||||
final bool hasReflection;
|
||||
final GrowthStage growthStage;
|
||||
|
||||
bool get hasIntention => intention.isNotEmpty;
|
||||
bool get hasFocus => focusMinutes > 0;
|
||||
bool get hasHabits => completedHabits > 0;
|
||||
bool get isLoopClosed => hasIntention && hasFocus && hasReflection;
|
||||
int get completedSteps =>
|
||||
[
|
||||
hasIntention,
|
||||
hasFocus,
|
||||
hasHabits,
|
||||
hasReflection,
|
||||
].where((complete) => complete).length;
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
enum PremiumState { free, premium }
|
||||
|
||||
enum AdPlacement { statsBottom, reflectionComplete }
|
||||
|
||||
sealed class PurchaseResult {
|
||||
const PurchaseResult();
|
||||
|
||||
static const unavailable = PurchaseUnavailable();
|
||||
}
|
||||
|
||||
class PurchaseUnavailable extends PurchaseResult {
|
||||
const PurchaseUnavailable();
|
||||
}
|
||||
|
||||
abstract interface class MonetizationProvider {
|
||||
bool get adsEnabled;
|
||||
bool get billingEnabled;
|
||||
Stream<PremiumState> premiumState();
|
||||
Future<void> showInterstitialIfEligible(AdPlacement placement);
|
||||
Future<PurchaseResult> startPurchase(String productId);
|
||||
Future<void> restorePurchases();
|
||||
}
|
||||
|
||||
class NoopMonetizationProvider implements MonetizationProvider {
|
||||
const NoopMonetizationProvider();
|
||||
|
||||
@override
|
||||
bool get adsEnabled => false;
|
||||
|
||||
@override
|
||||
bool get billingEnabled => false;
|
||||
|
||||
@override
|
||||
Stream<PremiumState> premiumState() => Stream.value(PremiumState.free);
|
||||
|
||||
@override
|
||||
Future<void> showInterstitialIfEligible(AdPlacement placement) async {}
|
||||
|
||||
@override
|
||||
Future<PurchaseResult> startPurchase(String productId) async {
|
||||
return PurchaseResult.unavailable;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restorePurchases() async {}
|
||||
}
|
||||
+452
-148
@@ -1,122 +1,262 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_theme.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'app_bottom_sheet.dart';
|
||||
import 'app_busy_indicator.dart';
|
||||
import 'surface_panel.dart';
|
||||
|
||||
Future<void> showHabitDialog(
|
||||
BuildContext context,
|
||||
ValueChanged<String> onSubmit, {
|
||||
Future<bool> Function(String value) onSubmit, {
|
||||
String initialValue = '',
|
||||
}) async {
|
||||
var value = initialValue;
|
||||
await showDialog<void>(
|
||||
final l10n = context.l10n;
|
||||
await showTextEntrySheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(
|
||||
initialValue.isEmpty
|
||||
? context.l10n.addHabitTitle
|
||||
: context.l10n.renameHabitTitle,
|
||||
),
|
||||
content: TextFormField(
|
||||
initialValue: initialValue,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: context.l10n.habitHint,
|
||||
),
|
||||
onChanged: (nextValue) => value = nextValue,
|
||||
onFieldSubmitted: (value) {
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: initialValue.isEmpty ? l10n.addHabitTitle : l10n.renameHabitTitle,
|
||||
hintText: l10n.habitHint,
|
||||
initialValue: initialValue,
|
||||
icon: Icons.add_task_rounded,
|
||||
accentBuilder: (context) => FocusSeedVisuals.of(context).habit,
|
||||
onSubmit: onSubmit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showIntentionDialog(
|
||||
BuildContext context,
|
||||
ValueChanged<String> onSubmit, {
|
||||
Future<bool> Function(String value) onSubmit, {
|
||||
String initialValue = '',
|
||||
}) async {
|
||||
var value = initialValue;
|
||||
await showDialog<void>(
|
||||
final l10n = context.l10n;
|
||||
await showTextEntrySheet(
|
||||
context: context,
|
||||
title:
|
||||
initialValue.isEmpty ? l10n.setIntentionTitle : l10n.editIntentionTitle,
|
||||
hintText: l10n.intentionHint,
|
||||
initialValue: initialValue,
|
||||
icon: Icons.spa_rounded,
|
||||
maxLength: 90,
|
||||
maxLines: 3,
|
||||
accentBuilder: (context) => FocusSeedVisuals.of(context).action,
|
||||
onSubmit: onSubmit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTextEntrySheet({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String hintText,
|
||||
required Future<bool> Function(String value) onSubmit,
|
||||
required IconData icon,
|
||||
required Color Function(BuildContext context) accentBuilder,
|
||||
String initialValue = '',
|
||||
int? maxLength,
|
||||
int minLines = 1,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(
|
||||
initialValue.isEmpty
|
||||
? context.l10n.setIntentionTitle
|
||||
: context.l10n.editIntentionTitle,
|
||||
),
|
||||
content: TextFormField(
|
||||
initialValue: initialValue,
|
||||
autofocus: true,
|
||||
maxLength: 90,
|
||||
maxLines: 3,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(hintText: context.l10n.intentionHint),
|
||||
onChanged: (nextValue) => value = nextValue,
|
||||
onFieldSubmitted: (value) {
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.save),
|
||||
),
|
||||
],
|
||||
(context) => _TextEntrySheet(
|
||||
title: title,
|
||||
hintText: hintText,
|
||||
initialValue: initialValue,
|
||||
maxLength: maxLength,
|
||||
minLines: minLines,
|
||||
maxLines: maxLines,
|
||||
icon: icon,
|
||||
accent: accentBuilder(context),
|
||||
onSubmit: onSubmit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _TextEntrySheet extends StatefulWidget {
|
||||
const _TextEntrySheet({
|
||||
required this.title,
|
||||
required this.hintText,
|
||||
required this.initialValue,
|
||||
required this.icon,
|
||||
required this.accent,
|
||||
required this.onSubmit,
|
||||
this.maxLength,
|
||||
this.minLines = 1,
|
||||
this.maxLines = 1,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String hintText;
|
||||
final String initialValue;
|
||||
final IconData icon;
|
||||
final Color accent;
|
||||
final Future<bool> Function(String value) onSubmit;
|
||||
final int? maxLength;
|
||||
final int minLines;
|
||||
final int maxLines;
|
||||
|
||||
@override
|
||||
State<_TextEntrySheet> createState() => _TextEntrySheetState();
|
||||
}
|
||||
|
||||
class _TextEntrySheetState extends State<_TextEntrySheet> {
|
||||
late final TextEditingController _controller;
|
||||
bool _saving = false;
|
||||
String? _errorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_saving) {
|
||||
return;
|
||||
}
|
||||
final value = _controller.text.trim();
|
||||
if (value.isEmpty) {
|
||||
setState(() => _errorText = context.l10n.textEntryRequired);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_saving = true;
|
||||
_errorText = null;
|
||||
});
|
||||
final saved = await widget.onSubmit(value);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (saved) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_saving = false;
|
||||
_errorText = context.l10n.saveFailed;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final foreground =
|
||||
ThemeData.estimateBrightnessForColor(widget.accent) == Brightness.dark
|
||||
? Colors.white
|
||||
: scheme.onSurface;
|
||||
return AppBottomSheetScaffold(
|
||||
title: widget.title,
|
||||
icon: widget.icon,
|
||||
accent: widget.accent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SurfacePanel(
|
||||
radius: 18,
|
||||
tone: SurfacePanelTone.paper,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
maxLength: widget.maxLength,
|
||||
minLines: widget.minLines,
|
||||
maxLines: widget.maxLines,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
textInputAction:
|
||||
widget.maxLines == 1
|
||||
? TextInputAction.done
|
||||
: TextInputAction.newline,
|
||||
onChanged: (_) {
|
||||
if (_errorText == null) {
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
setState(() => _errorText = null);
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
if (widget.maxLines == 1) {
|
||||
unawaited(_submit());
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: widget.hintText,
|
||||
errorText: _errorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FilledButton(
|
||||
onPressed:
|
||||
_saving || _controller.text.trim().isEmpty
|
||||
? null
|
||||
: _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: widget.accent,
|
||||
foregroundColor: foreground,
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
textStyle: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
child:
|
||||
_saving
|
||||
? AppBusyIndicator(
|
||||
semanticLabel: l10n.savingLabel,
|
||||
color: foreground,
|
||||
)
|
||||
: Text(l10n.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> confirmDeleteHabit(
|
||||
BuildContext context,
|
||||
VoidCallback onConfirm,
|
||||
) async {
|
||||
await showDialog<void>(
|
||||
await showConfirmSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(context.l10n.deleteHabitTitle),
|
||||
content: Text(context.l10n.deleteHabitBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onConfirm();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: context.l10n.deleteHabitTitle,
|
||||
body: context.l10n.deleteHabitBody,
|
||||
confirmLabel: context.l10n.delete,
|
||||
icon: Icons.delete_outline_rounded,
|
||||
accentBuilder: (context) => Theme.of(context).colorScheme.error,
|
||||
onConfirm: onConfirm,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,26 +264,14 @@ Future<void> confirmClearToday(
|
||||
BuildContext context,
|
||||
VoidCallback onConfirm,
|
||||
) async {
|
||||
await showDialog<void>(
|
||||
await showConfirmSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(context.l10n.clearTodayDataTitle),
|
||||
content: Text(context.l10n.clearTodayDataBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onConfirm();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.clearToday),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: context.l10n.clearTodayDataTitle,
|
||||
body: context.l10n.clearTodayDataBody,
|
||||
confirmLabel: context.l10n.clearToday,
|
||||
icon: Icons.refresh_rounded,
|
||||
accentBuilder: (context) => FocusSeedVisuals.of(context).warning,
|
||||
onConfirm: onConfirm,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,60 +279,236 @@ Future<void> confirmClearData(
|
||||
BuildContext context,
|
||||
VoidCallback onConfirm,
|
||||
) async {
|
||||
await showDialog<void>(
|
||||
await showConfirmSheet(
|
||||
context: context,
|
||||
title: context.l10n.clearAllDataTitle,
|
||||
body: context.l10n.clearAllDataBody,
|
||||
confirmLabel: context.l10n.deleteAll,
|
||||
icon: Icons.delete_forever_rounded,
|
||||
accentBuilder: (context) => Theme.of(context).colorScheme.error,
|
||||
onConfirm: onConfirm,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showConfirmSheet({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String body,
|
||||
required String confirmLabel,
|
||||
required IconData icon,
|
||||
required Color Function(BuildContext context) accentBuilder,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
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.deleteAll),
|
||||
),
|
||||
],
|
||||
(context) => _ConfirmSheet(
|
||||
title: title,
|
||||
body: body,
|
||||
confirmLabel: confirmLabel,
|
||||
icon: icon,
|
||||
accent: accentBuilder(context),
|
||||
onConfirm: onConfirm,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ConfirmSheet extends StatelessWidget {
|
||||
const _ConfirmSheet({
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.confirmLabel,
|
||||
required this.icon,
|
||||
required this.accent,
|
||||
required this.onConfirm,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
final String confirmLabel;
|
||||
final IconData icon;
|
||||
final Color accent;
|
||||
final VoidCallback onConfirm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
return AppBottomSheetScaffold(
|
||||
title: title,
|
||||
icon: icon,
|
||||
accent: accent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SurfacePanel(
|
||||
radius: 18,
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||
color: Color.alphaBlend(
|
||||
accent.withValues(alpha: 0.045),
|
||||
scheme.surfaceContainerLowest,
|
||||
),
|
||||
child: Text(
|
||||
body,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
height: 1.42,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_ConfirmActions(
|
||||
accent: accent,
|
||||
confirmLabel: confirmLabel,
|
||||
onConfirm: onConfirm,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showPrivacySheet(BuildContext context) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder:
|
||||
(context) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
(context) => AppBottomSheetScaffold(
|
||||
title: context.l10n.privacyTitle,
|
||||
subtitle: context.l10n.privacySubtitle,
|
||||
icon: Icons.lock_outline_rounded,
|
||||
accent: FocusSeedVisuals.of(context).privacy,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.privacyTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
_PrivacyPoint(
|
||||
icon: Icons.phone_android_rounded,
|
||||
text: context.l10n.privacyBodyOne,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(context.l10n.privacyBodyOne),
|
||||
const SizedBox(height: 12),
|
||||
Text(context.l10n.privacyBodyTwo),
|
||||
_PrivacyPoint(
|
||||
icon: Icons.cleaning_services_rounded,
|
||||
text: context.l10n.privacyBodyTwo,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showSnack(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
||||
class _PrivacyPoint extends StatelessWidget {
|
||||
const _PrivacyPoint({required this.icon, required this.text});
|
||||
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
return SurfacePanel(
|
||||
radius: 18,
|
||||
padding: const EdgeInsets.fromLTRB(13, 13, 13, 13),
|
||||
color: Color.alphaBlend(
|
||||
visuals.privacy.withValues(alpha: 0.045),
|
||||
scheme.surfaceContainerLowest,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: visuals.privacy, size: 21),
|
||||
const SizedBox(width: 11),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
height: 1.42,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showSnack(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 92),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ConfirmActions extends StatelessWidget {
|
||||
const _ConfirmActions({
|
||||
required this.accent,
|
||||
required this.confirmLabel,
|
||||
required this.onConfirm,
|
||||
});
|
||||
|
||||
final Color accent;
|
||||
final String confirmLabel;
|
||||
final VoidCallback onConfirm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final foreground =
|
||||
ThemeData.estimateBrightnessForColor(accent) == Brightness.dark
|
||||
? Colors.white
|
||||
: scheme.onSurface;
|
||||
final confirmButton = FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: accent,
|
||||
foregroundColor: foreground,
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
textStyle: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
onConfirm();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(confirmLabel, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
);
|
||||
final cancelButton = TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.cancel),
|
||||
);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textScale = MediaQuery.textScalerOf(context).scale(1);
|
||||
if (constraints.maxWidth < 330 || textScale > 1.25) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [confirmButton, const SizedBox(height: 8), cancelButton],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: cancelButton),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(flex: 2, child: confirmButton),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class FocusSeedHeader extends StatelessWidget {
|
||||
const FocusSeedHeader({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.showBrand = true,
|
||||
this.isSaving = false,
|
||||
this.trailing,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool showBrand;
|
||||
final bool isSaving;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = context.l10n;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 14, 20, 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showBrand) ...[
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textScale =
|
||||
MediaQuery.textScalerOf(context).scale(14) / 14;
|
||||
final compact = constraints.maxWidth < 340 || textScale > 1.3;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withValues(
|
||||
alpha: 0.12,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.spa_rounded,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 21,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.brandName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child:
|
||||
isSaving
|
||||
? _SaveStatus(
|
||||
key: const ValueKey('saving'),
|
||||
icon: Icons.sync_rounded,
|
||||
label: l10n.saving,
|
||||
compact: compact,
|
||||
)
|
||||
: _SaveStatus(
|
||||
key: const ValueKey('saved'),
|
||||
icon: Icons.smartphone_outlined,
|
||||
label: l10n.local,
|
||||
compact: compact,
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
trailing!,
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontSize: 24),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SaveStatus extends StatelessWidget {
|
||||
const _SaveStatus({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.compact,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Semantics(
|
||||
label: label,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 15, color: theme.colorScheme.primary),
|
||||
if (!compact) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: theme.textTheme.labelSmall),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_theme.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/growth_stage.dart';
|
||||
|
||||
class GrowthObject extends StatelessWidget {
|
||||
const GrowthObject({
|
||||
required this.stage,
|
||||
this.size = 148,
|
||||
this.showAmbientHalo = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final GrowthStage stage;
|
||||
final double size;
|
||||
final bool showAmbientHalo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reduceMotion = MediaQuery.of(context).disableAnimations;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final image = Image.asset(
|
||||
stage.assetPath,
|
||||
key: ValueKey(stage),
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
semanticLabel: localizedGrowthSemantic(context.l10n, stage),
|
||||
);
|
||||
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (showAmbientHalo)
|
||||
Container(
|
||||
width: size * 0.76,
|
||||
height: size * 0.76,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: visuals.ambientTint,
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration:
|
||||
reduceMotion
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 420),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: (child, animation) {
|
||||
if (reduceMotion) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
}
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: Tween(begin: 0.94, end: 1.0).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: image,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_theme.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'app_motion.dart';
|
||||
|
||||
class LoadingView extends StatelessWidget {
|
||||
const LoadingView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
final theme = Theme.of(context);
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final l10n = context.l10n;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_LoadingMark(color: visuals.focus),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
l10n.loadingLocalData,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoadingMark extends StatefulWidget {
|
||||
const _LoadingMark({required this.color});
|
||||
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
State<_LoadingMark> createState() => _LoadingMarkState();
|
||||
}
|
||||
|
||||
class _LoadingMarkState extends State<_LoadingMark>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1100),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (AppMotion.reduced(context)) {
|
||||
_controller.stop();
|
||||
_controller.value = 1;
|
||||
return;
|
||||
}
|
||||
if (!_controller.isAnimating) {
|
||||
_controller.repeat(reverse: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (AppMotion.reduced(context)) {
|
||||
return CustomPaint(
|
||||
size: const Size.square(44),
|
||||
painter: _LoadingMarkPainter(color: widget.color, progress: 1),
|
||||
);
|
||||
}
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final progress =
|
||||
0.58 + Curves.easeInOut.transform(_controller.value) * 0.42;
|
||||
return CustomPaint(
|
||||
size: const Size.square(44),
|
||||
painter: _LoadingMarkPainter(color: widget.color, progress: progress),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoadingMarkPainter extends CustomPainter {
|
||||
const _LoadingMarkPainter({required this.color, required this.progress});
|
||||
|
||||
final Color color;
|
||||
final double progress;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = size.center(Offset.zero);
|
||||
final seedRect = Rect.fromCenter(
|
||||
center: center,
|
||||
width: size.width * (0.52 + progress * 0.08),
|
||||
height: size.height * 0.42,
|
||||
);
|
||||
canvas.drawOval(seedRect, Paint()..color = color.withValues(alpha: 0.13));
|
||||
canvas.drawOval(
|
||||
seedRect.deflate(1),
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.8
|
||||
..color = color.withValues(alpha: 0.28 + progress * 0.35),
|
||||
);
|
||||
final path =
|
||||
Path()
|
||||
..moveTo(size.width * 0.28, size.height * 0.54)
|
||||
..quadraticBezierTo(
|
||||
size.width * 0.48,
|
||||
size.height * 0.40,
|
||||
size.width * 0.72,
|
||||
size.height * 0.48,
|
||||
);
|
||||
final metric = path.computeMetrics().first;
|
||||
canvas.drawPath(
|
||||
metric.extractPath(0, metric.length * progress.clamp(0.08, 1)),
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.6
|
||||
..strokeCap = StrokeCap.round
|
||||
..color = color,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _LoadingMarkPainter oldDelegate) {
|
||||
return oldDelegate.color != color || oldDelegate.progress != progress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SeedIllustrationTone { garden, archive, data }
|
||||
|
||||
class SeedIllustration extends StatelessWidget {
|
||||
const SeedIllustration({
|
||||
this.tone = SeedIllustrationTone.garden,
|
||||
this.size = 118,
|
||||
this.hero = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SeedIllustrationTone tone;
|
||||
final double size;
|
||||
final bool hero;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final accent = switch (tone) {
|
||||
SeedIllustrationTone.garden => scheme.primary,
|
||||
SeedIllustrationTone.archive => scheme.secondary,
|
||||
SeedIllustrationTone.data => scheme.tertiary,
|
||||
};
|
||||
final softAccent = accent.withValues(alpha: 0.18);
|
||||
|
||||
return ExcludeSemantics(
|
||||
child: SizedBox.square(
|
||||
dimension: size,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
softAccent.withValues(alpha: hero ? 0.92 : 1),
|
||||
scheme.surfaceContainerHigh.withValues(alpha: 0.28),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hero)
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _OrbitPainter(
|
||||
color: accent.withValues(alpha: 0.30),
|
||||
quietColor: scheme.outlineVariant.withValues(alpha: 0.30),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hero)
|
||||
Positioned(
|
||||
left: size * 0.08,
|
||||
top: size * 0.08,
|
||||
child: _Sparkle(size: size * 0.13, color: accent),
|
||||
),
|
||||
if (hero)
|
||||
Positioned(
|
||||
right: size * 0.14,
|
||||
bottom: size * 0.12,
|
||||
child: _Sparkle(size: size * 0.10, color: scheme.tertiary),
|
||||
),
|
||||
Positioned(
|
||||
bottom: size * 0.16,
|
||||
child: Container(
|
||||
width: size * (hero ? 0.72 : 0.62),
|
||||
height: size * (hero ? 0.31 : 0.28),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHighest.withValues(alpha: 0.78),
|
||||
borderRadius: BorderRadius.circular(size),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: hero ? 0.16 : 0.10),
|
||||
blurRadius: hero ? 26 : 18,
|
||||
offset: Offset(0, hero ? 12 : 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: size * (hero ? 0.15 : 0.18),
|
||||
left: size * (hero ? 0.24 : 0.28),
|
||||
child: Transform.rotate(
|
||||
angle: -0.62,
|
||||
child: _LeafBlob(
|
||||
width: size * (hero ? 0.30 : 0.26),
|
||||
height: size * (hero ? 0.48 : 0.42),
|
||||
color: accent.withValues(alpha: 0.78),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: size * (hero ? 0.12 : 0.16),
|
||||
right: size * (hero ? 0.24 : 0.28),
|
||||
child: Transform.rotate(
|
||||
angle: 0.62,
|
||||
child: _LeafBlob(
|
||||
width: size * (hero ? 0.33 : 0.28),
|
||||
height: size * (hero ? 0.52 : 0.45),
|
||||
color: scheme.primaryContainer.withValues(alpha: 0.86),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hero)
|
||||
Positioned(
|
||||
top: size * 0.25,
|
||||
right: size * 0.36,
|
||||
child: Transform.rotate(
|
||||
angle: 0.10,
|
||||
child: _LeafBlob(
|
||||
width: size * 0.18,
|
||||
height: size * 0.30,
|
||||
color: scheme.tertiary.withValues(alpha: 0.62),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: size * 0.44,
|
||||
child: Container(
|
||||
width: size * (hero ? 0.09 : 0.08),
|
||||
height: size * (hero ? 0.32 : 0.28),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.primary.withValues(alpha: 0.78),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: size * 0.26,
|
||||
child: Transform.rotate(
|
||||
angle: 0.18,
|
||||
child: Container(
|
||||
width: size * (hero ? 0.21 : 0.18),
|
||||
height: size * (hero ? 0.31 : 0.28),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.tertiaryContainer.withValues(alpha: 0.90),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
border: Border.all(
|
||||
color: scheme.tertiary.withValues(alpha: 0.28),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accent.withValues(alpha: 0.22),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: size * 0.10,
|
||||
top: size * 0.30,
|
||||
child: _FloatingDot(size: size * 0.09, color: accent),
|
||||
),
|
||||
Positioned(
|
||||
left: size * 0.12,
|
||||
bottom: size * 0.34,
|
||||
child: _FloatingDot(size: size * 0.065, color: scheme.secondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeafBlob extends StatelessWidget {
|
||||
const _LeafBlob({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(width),
|
||||
topRight: Radius.circular(width * 0.35),
|
||||
bottomLeft: Radius.circular(width * 0.35),
|
||||
bottomRight: Radius.circular(width),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.10),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FloatingDot extends StatelessWidget {
|
||||
const _FloatingDot({required this.size, required this.color});
|
||||
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color.withValues(alpha: 0.42),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.22),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Sparkle extends StatelessWidget {
|
||||
const _Sparkle({required this.size, required this.color});
|
||||
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: CustomPaint(painter: _SparklePainter(color)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SparklePainter extends CustomPainter {
|
||||
const _SparklePainter(this.color);
|
||||
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()..color = color.withValues(alpha: 0.72);
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final path =
|
||||
Path()
|
||||
..moveTo(center.dx, 0)
|
||||
..quadraticBezierTo(
|
||||
center.dx * 1.18,
|
||||
center.dy * 0.78,
|
||||
size.width,
|
||||
center.dy,
|
||||
)
|
||||
..quadraticBezierTo(
|
||||
center.dx * 1.18,
|
||||
center.dy * 1.18,
|
||||
center.dx,
|
||||
size.height,
|
||||
)
|
||||
..quadraticBezierTo(center.dx * 0.78, center.dy * 1.18, 0, center.dy)
|
||||
..quadraticBezierTo(center.dx * 0.78, center.dy * 0.78, center.dx, 0);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _SparklePainter oldDelegate) {
|
||||
return oldDelegate.color != color;
|
||||
}
|
||||
}
|
||||
|
||||
class _OrbitPainter extends CustomPainter {
|
||||
const _OrbitPainter({required this.color, required this.quietColor});
|
||||
|
||||
final Color color;
|
||||
final Color quietColor;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect =
|
||||
Offset(size.width * 0.08, size.height * 0.12) &
|
||||
Size(size.width * 0.84, size.height * 0.72);
|
||||
final paint =
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = size.shortestSide * 0.018
|
||||
..strokeCap = StrokeCap.round
|
||||
..color = quietColor;
|
||||
canvas.drawArc(rect, -0.12, 4.8, false, paint);
|
||||
paint.color = color;
|
||||
canvas.drawArc(rect, 2.8, 1.25, false, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _OrbitPainter oldDelegate) {
|
||||
return oldDelegate.color != color || oldDelegate.quietColor != quietColor;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsTile extends StatelessWidget {
|
||||
const SettingsTile({
|
||||
import 'workbench_ui.dart';
|
||||
|
||||
class SettingsSection extends StatelessWidget {
|
||||
const SettingsSection({
|
||||
required this.title,
|
||||
required this.children,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
WorkbenchPanel(
|
||||
accent: Theme.of(context).colorScheme.primary,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var i = 0; i < children.length; i++) ...[
|
||||
if (i > 0) const Divider(indent: 16, endIndent: 16, height: 1),
|
||||
children[i],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsRow extends StatelessWidget {
|
||||
const SettingsRow({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
this.destructive = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
final bool destructive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = destructive ? Theme.of(context).colorScheme.error : null;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(icon, color: color),
|
||||
title: Text(title, style: TextStyle(color: color)),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: onTap,
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 13, 10, 13),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.11),
|
||||
borderRadius: BorderRadius.circular(kWorkbenchRadius),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_theme.dart';
|
||||
import 'pressable_scale.dart';
|
||||
|
||||
enum SurfacePanelTone { plain, paper, wash }
|
||||
|
||||
class SurfacePanel extends StatelessWidget {
|
||||
const SurfacePanel({
|
||||
@@ -9,6 +12,8 @@ class SurfacePanel extends StatelessWidget {
|
||||
this.margin,
|
||||
this.color,
|
||||
this.borderColor,
|
||||
this.tone = SurfacePanelTone.plain,
|
||||
this.radius = 12,
|
||||
this.elevated = false,
|
||||
this.onTap,
|
||||
super.key,
|
||||
@@ -19,6 +24,8 @@ class SurfacePanel extends StatelessWidget {
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final Color? color;
|
||||
final Color? borderColor;
|
||||
final SurfacePanelTone tone;
|
||||
final double radius;
|
||||
final bool elevated;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@@ -26,35 +33,64 @@ class SurfacePanel extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final effectiveColor =
|
||||
color ??
|
||||
switch (tone) {
|
||||
SurfacePanelTone.plain => scheme.surfaceContainerLowest,
|
||||
SurfacePanelTone.paper =>
|
||||
scheme.brightness == Brightness.dark
|
||||
? scheme.surfaceContainerLow
|
||||
: AppTheme.paper,
|
||||
SurfacePanelTone.wash =>
|
||||
scheme.brightness == Brightness.dark
|
||||
? scheme.surfaceContainerLow
|
||||
: scheme.surfaceContainerLowest,
|
||||
};
|
||||
final effectiveBorderColor =
|
||||
borderColor ??
|
||||
(tone == SurfacePanelTone.wash
|
||||
? visuals.focus.withValues(alpha: 0.06)
|
||||
: scheme.outlineVariant.withValues(alpha: 0.0));
|
||||
final decoration = BoxDecoration(
|
||||
color: color ?? theme.colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: borderColor ?? theme.colorScheme.outlineVariant,
|
||||
),
|
||||
boxShadow:
|
||||
elevated
|
||||
? [
|
||||
BoxShadow(
|
||||
color: visuals.shadow,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: effectiveColor,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
border:
|
||||
effectiveBorderColor.a == 0
|
||||
? null
|
||||
: Border.all(color: effectiveBorderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: visuals.shadow.withValues(
|
||||
alpha:
|
||||
theme.colorScheme.brightness == Brightness.dark
|
||||
? elevated
|
||||
? 0.22
|
||||
: 0.11
|
||||
: elevated
|
||||
? 0.11
|
||||
: 0.045,
|
||||
),
|
||||
blurRadius: elevated ? 22 : 12,
|
||||
offset: Offset(0, elevated ? 8 : 4),
|
||||
),
|
||||
],
|
||||
);
|
||||
final content = Padding(padding: padding, child: child);
|
||||
return Container(
|
||||
margin: margin,
|
||||
decoration: decoration,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child:
|
||||
onTap == null
|
||||
? content
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(onTap: onTap, child: content),
|
||||
),
|
||||
return PressableScale(
|
||||
enabled: onTap != null,
|
||||
child: Container(
|
||||
margin: margin,
|
||||
decoration: decoration,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child:
|
||||
onTap == null
|
||||
? content
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(onTap: onTap, child: content),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user