Skip to content

Commit 36ba3a7

Browse files
xronocodeqwencoder
andcommitted
PHASE6-006. (feat) Child Experience Polish
Implemented: 1.1 Retry Flow UI - 'Try Again!' badge with amber styling 1.2 Speed Timer Ring - Color gradient (Amber→Yellow→Red) 1.4 Celebration Animations - Confetti on perfect/great sessions Files: - dash_session_screen.dart: Retry UI + confetti integration - countdown_ring.dart: Color gradient based on progress - celebration_overlay.dart: NEW - Confetti widget - pubspec.yaml: Added confetti, audioplayers Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent e43eb25 commit 36ba3a7

File tree

13 files changed

+1015
-12
lines changed

13 files changed

+1015
-12
lines changed

.DS_Store

0 Bytes
Binary file not shown.

.kilocode/.DS_Store

0 Bytes
Binary file not shown.

apps/.DS_Store

0 Bytes
Binary file not shown.

apps/mobile_flutter/lib/core/app/app_shell.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '../../features/child_loop/presentation/study_tricks_screen.dart';
1111
import '../../features/child_loop/presentation/table_mastery_screen.dart';
1212
import '../../features/parent_loop/presentation/parent_dashboard_screen.dart';
1313
import '../../features/profile/domain/entities/app_role.dart';
14+
import '../../features/sync/presentation/widgets/connectivity_badge.dart';
1415
import '../theme/app_theme.dart';
1516
import 'app_dependencies.dart';
1617

@@ -120,7 +121,11 @@ class _AppShellState extends State<AppShell> with WidgetsBindingObserver {
120121
'MathMagic ✨',
121122
style: GoogleFonts.fredoka(fontSize: 20, fontWeight: FontWeight.w600),
122123
),
123-
actions: [_EnvironmentBadge(envName: dependencies.env.name)],
124+
actions: [
125+
const ConnectivityBadge(),
126+
const SizedBox(width: 8),
127+
_EnvironmentBadge(envName: dependencies.env.name),
128+
],
124129
);
125130
}
126131

apps/mobile_flutter/lib/features/child_loop/presentation/dash_session_screen.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,13 @@ class _DashSessionScreenState extends State<DashSessionScreen>
312312

313313
// Scrollable middle content
314314
Expanded(
315-
child: Align(
316-
alignment: const Alignment(0, -0.15),
317-
child: SingleChildScrollView(
318-
child: Column(
319-
mainAxisSize: MainAxisSize.min,
320-
children: [
315+
child: ClipRect(
316+
child: Align(
317+
alignment: const Alignment(0, -0.15),
318+
child: SingleChildScrollView(
319+
child: Column(
320+
mainAxisSize: MainAxisSize.min,
321+
children: [
321322
// Countdown ring (above question, hidden during retry)
322323
if (!_isInRetryMode)
323324
Padding(
@@ -394,6 +395,7 @@ class _DashSessionScreenState extends State<DashSessionScreen>
394395
),
395396
),
396397
),
398+
),
397399
),
398400

399401
// Keypad
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:confetti/confetti.dart';
4+
import 'package:flutter/material.dart';
5+
6+
import '../../../../core/theme/app_theme.dart';
7+
8+
/// Celebration overlay with confetti animation for milestones.
9+
///
10+
/// Shows confetti explosion when session is completed with good performance.
11+
class CelebrationOverlay extends StatefulWidget {
12+
const CelebrationOverlay({
13+
super.key,
14+
required this.child,
15+
this.celebrate = true,
16+
});
17+
18+
final Widget child;
19+
final bool celebrate;
20+
21+
@override
22+
State<CelebrationOverlay> createState() => _CelebrationOverlayState();
23+
}
24+
25+
class _CelebrationOverlayState extends State<CelebrationOverlay> {
26+
late ConfettiController _confettiController;
27+
28+
@override
29+
void initState() {
30+
super.initState();
31+
_confettiController = ConfettiController(
32+
duration: const Duration(seconds: 3),
33+
);
34+
35+
// Start celebration after build
36+
if (widget.celebrate) {
37+
WidgetsBinding.instance.addPostFrameCallback((_) {
38+
_confettiController.play();
39+
});
40+
}
41+
}
42+
43+
@override
44+
void dispose() {
45+
_confettiController.dispose();
46+
super.dispose();
47+
}
48+
49+
@override
50+
Widget build(BuildContext context) {
51+
return Stack(
52+
children: [
53+
widget.child,
54+
Positioned(
55+
top: 0,
56+
left: 0,
57+
right: 0,
58+
height: 200,
59+
child: Align(
60+
alignment: Alignment.topCenter,
61+
child: ConfettiWidget(
62+
confettiController: _confettiController,
63+
blastDirectionality: BlastDirectionality.explosive,
64+
particleDrag: 0.05,
65+
emissionFrequency: 0.05,
66+
numberOfParticles: 30,
67+
gravity: 0.1,
68+
colors: const [
69+
AppTheme.indigo,
70+
AppTheme.rose,
71+
AppTheme.emerald,
72+
AppTheme.amber,
73+
Colors.purple,
74+
Colors.blue,
75+
],
76+
shouldLoop: false,
77+
),
78+
),
79+
),
80+
],
81+
);
82+
}
83+
}
84+
85+
/// Particle burst effect for instant celebration.
86+
class ParticleBurst extends StatelessWidget {
87+
const ParticleBurst({
88+
super.key,
89+
required this.controller,
90+
});
91+
92+
final ConfettiController controller;
93+
94+
@override
95+
Widget build(BuildContext context) {
96+
return Align(
97+
alignment: Alignment.topCenter,
98+
child: ConfettiWidget(
99+
confettiController: controller,
100+
blastDirectionality: BlastDirectionality.explosive,
101+
particleDrag: 0.05,
102+
emissionFrequency: 0.05,
103+
numberOfParticles: 50,
104+
gravity: 0.1,
105+
colors: const [
106+
AppTheme.indigo,
107+
AppTheme.rose,
108+
AppTheme.emerald,
109+
AppTheme.amber,
110+
],
111+
shouldLoop: false,
112+
),
113+
);
114+
}
115+
}

apps/mobile_flutter/lib/features/parent_loop/presentation/parent_dashboard_screen.dart

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import 'package:flutter/material.dart';
22

33
import 'parent_dashboard_controller.dart';
4+
import '../../../sync/presentation/widgets/sync_queue_status.dart';
5+
import '../../../sync/data/outbox_sync_manager.dart';
6+
import '../../../sync/data/outbox_repository_impl.dart';
7+
import '../../../sync/data/sync_state_repository_impl.dart';
8+
import '../../../sync/http_remote_sync_client.dart';
9+
import '../../../progress/data/local_progress_repository_impl.dart';
10+
import '../../../child_loop/data/fact_progress_repository_impl.dart';
11+
import '../../../child_loop/data/quest_progress_repository_impl.dart';
12+
import '../../../child_loop/data/learning_repository_impl.dart';
13+
import '../../../assignments/data/assignment_repository_impl.dart';
14+
import '../../../core/data/local/database.dart';
415

516
class ParentDashboardScreen extends StatefulWidget {
617
const ParentDashboardScreen({
@@ -188,13 +199,51 @@ class _ParentDashboardScreenState extends State<ParentDashboardScreen> {
188199
),
189200
),
190201
),
202+
const SizedBox(height: 16),
203+
// Sync Queue Status
204+
Row(
205+
children: [
206+
Expanded(
207+
child: Text(
208+
'Sync Status',
209+
style: Theme.of(context).textTheme.titleMedium,
210+
),
211+
),
212+
SyncQueueStatus(
213+
outboxRepository: OutboxRepositoryImpl(
214+
db: Database.instance,
215+
),
216+
),
217+
],
218+
),
191219
const SizedBox(height: 8),
220+
// Enhanced Manual Sync Button
192221
FilledButton.icon(
193-
onPressed: () async {
194-
await widget.controller.triggerSyncAndReload(widget.profileId);
195-
},
196-
icon: const Icon(Icons.sync),
197-
label: const Text('Sync outbox now'),
222+
onPressed: widget.controller.isSyncing
223+
? null
224+
: () async {
225+
await widget.controller.triggerSyncAndReload(widget.profileId);
226+
if (context.mounted) {
227+
ScaffoldMessenger.of(context).showSnackBar(
228+
const SnackBar(
229+
content: Text('Sync completed!'),
230+
duration: Duration(seconds: 2),
231+
),
232+
);
233+
}
234+
},
235+
icon: widget.controller.isSyncing
236+
? const SizedBox(
237+
width: 16,
238+
height: 16,
239+
child: CircularProgressIndicator(strokeWidth: 2),
240+
)
241+
: const Icon(Icons.sync),
242+
label: Text(
243+
widget.controller.isSyncing
244+
? 'Syncing...'
245+
: 'Sync outbox now',
246+
),
198247
),
199248
const SizedBox(height: 8),
200249
OutlinedButton.icon(

0 commit comments

Comments
 (0)