mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:07:02 +01:00
- Committing a captured note (New note tap with a persisted draft) fires HapticFeedback.lightImpact and plays a ~600 ms checkmark beside the counter; empty drafts and keystrokes confirm nothing. - appRoute: one shared page route (fade + rise, AppDuration.slow) replaces all seven MaterialPageRoute sites; guard test keeps it that way. - Wizard progress bar animates on AppDuration.base / AppCurve.standard. - Everything honours disableAnimations; the haptic and the (static) checkmark still fire. Removes the finished dopamine-ux prompt (sound step rejected). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
2.5 KiB
Dart
92 lines
2.5 KiB
Dart
/// The checkmark confirming a capture; static under "remove animations".
|
|
library;
|
|
|
|
import 'package:design_system/design_system.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Plays once each time [captured] changes value.
|
|
class CaptureConfirmMark extends StatefulWidget {
|
|
/// Creates the mark.
|
|
const CaptureConfirmMark({required this.captured, super.key});
|
|
|
|
/// Key of the checkmark icon while it is visible.
|
|
static const ValueKey<String> iconKey = ValueKey('capture-confirm-icon');
|
|
|
|
/// Width always reserved, so the counter beside it never moves.
|
|
static const double slotWidth = 24;
|
|
|
|
/// Fade in, hold and fade out: three [AppDuration.base] thirds.
|
|
static final Duration duration = AppDuration.base * 3;
|
|
|
|
/// Counts captures; each change plays the confirmation.
|
|
final ValueListenable<int> captured;
|
|
|
|
@override
|
|
State<CaptureConfirmMark> createState() => _CaptureConfirmMarkState();
|
|
}
|
|
|
|
class _CaptureConfirmMarkState extends State<CaptureConfirmMark>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _controller = AnimationController(
|
|
vsync: this,
|
|
duration: CaptureConfirmMark.duration,
|
|
animationBehavior: AnimationBehavior.preserve,
|
|
);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
widget.captured.addListener(_play);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
widget.captured.removeListener(_play);
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _play() => _controller.forward(from: 0);
|
|
|
|
double _opacity({required bool reduced}) {
|
|
final t = _controller.value;
|
|
if (reduced || (t >= 1 / 3 && t < 2 / 3)) return 1;
|
|
if (t < 1 / 3) return AppCurve.decelerate.transform(t * 3);
|
|
return 1 - AppCurve.accelerate.transform((t - 2 / 3) * 3);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final reduced = MediaQuery.disableAnimationsOf(context);
|
|
return SizedBox(
|
|
width: CaptureConfirmMark.slotWidth,
|
|
child: AnimatedBuilder(
|
|
animation: _controller,
|
|
builder: (context, _) {
|
|
if (!_controller.isAnimating) return const SizedBox.shrink();
|
|
return Opacity(
|
|
opacity: _opacity(reduced: reduced),
|
|
child: const _CheckIcon(),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CheckIcon extends StatelessWidget {
|
|
const _CheckIcon();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = Theme.of(context).colorScheme.primary;
|
|
return Icon(
|
|
Icons.check_circle,
|
|
key: CaptureConfirmMark.iconKey,
|
|
size: 18,
|
|
color: color,
|
|
);
|
|
}
|
|
}
|