diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index c2dd560..1b6089d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,40 +1,59 @@
PODS:
+ - audioplayers_darwin (0.0.1):
+ - Flutter
+ - FlutterMacOS
- connectivity_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
+ - flutter_tts (0.0.1):
+ - Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - record_ios (1.1.0):
+ - Flutter
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
DEPENDENCIES:
+ - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
+ - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - record_ios (from `.symlinks/plugins/record_ios/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
EXTERNAL SOURCES:
+ audioplayers_darwin:
+ :path: ".symlinks/plugins/audioplayers_darwin/darwin"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
Flutter:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
+ flutter_tts:
+ :path: ".symlinks/plugins/flutter_tts/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
+ record_ios:
+ :path: ".symlinks/plugins/record_ios/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS:
+ audioplayers_darwin: 4f9ca89d92d3d21cec7ec580e78ca888e5fb68bd
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
+ flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
+ record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index d52c9d8..bb7e942 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -20,6 +20,8 @@
$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
+ NSMicrophoneUsageDescription
+ Some message to describe why you need this permission
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart
index 8ffe5cc..eb11b7a 100644
--- a/lib/core/routing/app_router.dart
+++ b/lib/core/routing/app_router.dart
@@ -1,7 +1,10 @@
+import 'package:base_flutter/features/lesson/lesson_screen.dart';
+import 'package:base_flutter/features/loading_lesson/loading_lesson_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
+import '../../features/home/models.dart';
import 'route_names.dart';
import 'route_paths.dart';
import 'route_guards.dart';
@@ -27,11 +30,39 @@ final routerProvider = Provider((ref) {
path: RoutePaths.home,
name: RouteNames.home,
pageBuilder: (context, state) => _buildPageWithTransition(
- child: const HomePage(),
+ child: const LevelMapScreen(),
state: state,
),
),
+ GoRoute(
+ path: RoutePaths.lesson,
+ name: RouteNames.lesson,
+ pageBuilder: (context, state) => _buildPageWithTransition(
+ child: const LoadingLessonScreen(),
+ state: state,
+ ),
+ routes: [
+ GoRoute(
+ path: 'test',
+ name: 'test',
+ pageBuilder: (context, state) {
+ final lessonResponse = state.extra as LessonResponse?;
+ if (lessonResponse == null) {
+ return _buildPageWithTransition(
+ child: const _PlaceholderPage(title: 'No lesson data provided'),
+ state: state,
+ );
+ }
+ return _buildPageWithTransition(
+ child: LessonScreen(lessonResponse: lessonResponse),
+ state: state,
+ );
+ },
+ ),
+ ]
+ ),
+
// Settings routes with nested navigation
GoRoute(
path: RoutePaths.settings,
diff --git a/lib/core/routing/route_names.dart b/lib/core/routing/route_names.dart
index 9e03c78..b2f82ed 100644
--- a/lib/core/routing/route_names.dart
+++ b/lib/core/routing/route_names.dart
@@ -7,6 +7,7 @@ class RouteNames {
static const String settings = 'settings';
static const String profile = 'profile';
static const String about = 'about';
+ static const String lesson = 'lesson';
// Auth routes
static const String login = 'login';
diff --git a/lib/core/routing/route_paths.dart b/lib/core/routing/route_paths.dart
index 04f9146..6b38cde 100644
--- a/lib/core/routing/route_paths.dart
+++ b/lib/core/routing/route_paths.dart
@@ -7,6 +7,7 @@ class RoutePaths {
static const String settings = '/settings';
static const String profile = '/profile';
static const String about = '/about';
+ static const String lesson = '/lesson';
// Auth routes
static const String login = '/auth/login';
diff --git a/lib/features/home/models.dart b/lib/features/home/models.dart
new file mode 100644
index 0000000..86d4293
--- /dev/null
+++ b/lib/features/home/models.dart
@@ -0,0 +1,79 @@
+
+
+class LessonResponse {
+ final String image; // base64 string
+ final String response;
+ final WidgetData widget;
+
+ LessonResponse({
+ required this.image,
+ required this.response,
+ required this.widget,
+ });
+
+ factory LessonResponse.fromJson(Map json) {
+ return LessonResponse(
+ image: json['image'] as String,
+ response: json['response'] as String,
+ widget: WidgetData.fromJson(json['widget']),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'image': image,
+ 'response': response,
+ 'widget': widget.toJson(),
+ };
+ }
+}
+
+class WidgetData {
+ final TapWidget tapToRecordWidget;
+ final TapWidget tapToSpeechWidget;
+
+ WidgetData({
+ required this.tapToRecordWidget,
+ required this.tapToSpeechWidget,
+ });
+
+ factory WidgetData.fromJson(Map json) {
+ return WidgetData(
+ tapToRecordWidget:
+ TapWidget.fromJson(json['tap_to_record_widget']),
+ tapToSpeechWidget:
+ TapWidget.fromJson(json['tap_to_speech_widget']),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'tap_to_record_widget': tapToRecordWidget.toJson(),
+ 'tap_to_speech_widget': tapToSpeechWidget.toJson(),
+ };
+ }
+}
+
+class TapWidget {
+ final String content;
+ final String position;
+
+ TapWidget({
+ required this.content,
+ required this.position,
+ });
+
+ factory TapWidget.fromJson(Map json) {
+ return TapWidget(
+ content: json['content'] as String,
+ position: json['position'] as String,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'content': content,
+ 'position': position,
+ };
+ }
+}
diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart
index 3e1b3d3..bf9548e 100644
--- a/lib/features/home/presentation/pages/home_page.dart
+++ b/lib/features/home/presentation/pages/home_page.dart
@@ -1,3 +1,5 @@
+import 'dart:math' as math;
+
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -6,506 +8,505 @@ import '../../../../core/routing/route_guards.dart';
import '../../../../shared/presentation/providers/app_providers.dart';
/// Home page with navigation to different features
-class HomePage extends ConsumerWidget {
- const HomePage({super.key});
+class LevelMapScreen extends StatefulWidget {
+ const LevelMapScreen({super.key});
@override
- Widget build(BuildContext context, WidgetRef ref) {
- final authState = ref.watch(authStateProvider);
- final themeMode = ref.watch(themeModeProvider);
+ State createState() => _LevelMapScreenState();
+}
+class _LevelMapScreenState extends State {
+ final ScrollController _scroll = ScrollController();
+
+ // ---- Dynamic level data (change these) ----
+ final int totalLevels = 60; // total levels you want to show
+ final int currentLevelIndex = 4; // 0-based index of current level
+
+ // ---- Layout knobs (tweak to taste) ----
+ static const double kNodeSpacing = 220; // px along the path between bubbles
+ static const double kStartOffset = 140; // first bubble offset from path start
+ static const double kVerticalStep = 270; // vertical descent per circle loop
+
+ // The scrollable map height is finite and derived from how many levels we show.
+ double get mapHeight {
+ return _calculateRequiredHeight(
+ totalLevels: totalLevels,
+ nodeSpacing: kNodeSpacing,
+ startOffset: kStartOffset,
+ );
+ }
+
+ /// Calculate the actual vertical height needed for circular path
+ double _calculateRequiredHeight({
+ required int totalLevels,
+ required double nodeSpacing,
+ required double startOffset,
+ }) {
+ // Path parameters (must match _buildCircularPath)
+ const double baseRadius = 0.32; // percentage of width
+ const double topPadding = 100.0;
+ const double bottomPadding = 100.0;
+
+ // Estimate circle circumference (approximation for cubic bezier circle)
+ final screenWidth = MediaQuery.of(context).size.width;
+ final radiusInPixels = screenWidth * baseRadius;
+ final approximateCircleLength = 2 * math.pi * radiusInPixels;
+
+ // Total path length needed for all nodes
+ final totalPathLength = startOffset + (totalLevels - 1) * nodeSpacing;
+
+ // Calculate how many circles we need
+ final numberOfCircles = (totalPathLength / approximateCircleLength).ceil();
+
+ // Calculate actual vertical height
+ final contentHeight = numberOfCircles * kVerticalStep;
+ final totalHeight = topPadding + contentHeight + bottomPadding;
+
+ return math.max(1200, totalHeight);
+ }
+
+ double? _currentLevelY; // used to auto-scroll near the current level
+
+ @override
+ void initState() {
+ super.initState();
+ // Auto-scroll after first layout
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ final y = _currentLevelY ?? 0;
+ final screenH = MediaQuery.of(context).size.height;
+ final target = (y - screenH * 0.45).clamp(0.0, mapHeight);
+ _scroll.animateTo(
+ target,
+ duration: const Duration(milliseconds: 600),
+ curve: Curves.easeOutCubic,
+ );
+ });
+ }
+
+ @override
+ void dispose() {
+ _scroll.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- title: const Text('Base Flutter App'),
- actions: [
- // Theme toggle button
- IconButton(
- onPressed: () {
- ref.read(themeModeProvider.notifier).toggleTheme();
+ backgroundColor: const Color(0xFFFFB43E), // warm island background
+ body: SafeArea(
+ child: Column(
+ children: [
+ _buildHeader(),
+ Expanded(
+ child: LayoutBuilder(
+ builder: (ctx, c) {
+ return SingleChildScrollView(
+ controller: _scroll,
+ child: SizedBox(
+ height: mapHeight, // <-- finite height
+ width: c.maxWidth,
+ child: LevelMap(
+ totalLevels: totalLevels,
+ currentIndex: currentLevelIndex,
+ nodeSpacing: kNodeSpacing,
+ startOffsetOnPath: kStartOffset,
+ verticalStep: kVerticalStep,
+ showFinishAtEnd: true,
+ onCurrentLevelPositionResolved: (dy) {
+ _currentLevelY = dy;
+ },
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ _buildBottomNav(),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() => Padding(
+ padding: const EdgeInsets.fromLTRB(8, 8, 8, 12),
+ child: Row(
+ children: [
+ IconButton(onPressed: () {}, icon: const Icon(Icons.arrow_back)),
+ const Expanded(
+ child: Text(
+ 'Kho báu đảo hoang',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
+ ),
+ ),
+ IconButton(onPressed: () {}, icon: const Icon(Icons.refresh)),
+ ],
+ ),
+ );
+
+ Widget _buildBottomNav() => Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(20), topRight: Radius.circular(20)),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 12,
+ offset: const Offset(0, -2),
+ )
+ ],
+ ),
+ child: SafeArea(
+ top: false,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: const [
+ _Nav(icon: Icons.home, label: 'Trang chủ'),
+ _Nav(icon: Icons.location_on, label: 'Khám phá', active: true),
+ _Nav(icon: Icons.bar_chart, label: 'Xếp hạng'),
+ _Nav(icon: Icons.people, label: 'Kết nối'),
+ _Nav(icon: Icons.person, label: 'Cá nhân'),
+ ],
+ ),
+ ),
+ ),
+ );
+}
+
+class _Nav extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final bool active;
+ const _Nav({required this.icon, required this.label, this.active = false, super.key});
+ @override
+ Widget build(BuildContext context) {
+ return Column(mainAxisSize: MainAxisSize.min, children: [
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: active ? Colors.orange : Colors.transparent,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Icon(icon, color: active ? Colors.white : Colors.grey)),
+ const SizedBox(height: 4),
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 12,
+ color: active ? Colors.orange : Colors.grey,
+ fontWeight: active ? FontWeight.bold : FontWeight.normal,
+ ),
+ ),
+ ]);
+ }
+}
+
+/// Draws the path and positions dynamic level nodes **along the path**.
+class LevelMap extends StatelessWidget {
+ final int totalLevels;
+ final int currentIndex;
+ final double nodeSpacing;
+ final double startOffsetOnPath;
+ final double verticalStep;
+ final bool showFinishAtEnd;
+ final ValueChanged? onCurrentLevelPositionResolved;
+
+ const LevelMap({
+ super.key,
+ required this.totalLevels,
+ required this.currentIndex,
+ required this.nodeSpacing,
+ required this.startOffsetOnPath,
+ required this.verticalStep,
+ this.showFinishAtEnd = false,
+ this.onCurrentLevelPositionResolved,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(builder: (ctx, c) {
+ final size = Size(c.maxWidth, c.maxHeight);
+
+ // Build a circular/spiral path that fills this finite canvas.
+ final path = _buildCircularPath(size, verticalStep);
+
+ // Measure the path
+ final metrics = path.computeMetrics().toList();
+ if (metrics.isEmpty) return const SizedBox.shrink();
+ final metric = metrics.first; // single contour
+ final totalLen = metric.length;
+
+ // Required length to host all visible levels
+ final requiredLen =
+ startOffsetOnPath + (math.max(1, totalLevels) - 1) * nodeSpacing;
+
+ // Clamp to available path length
+ final effectiveLen = requiredLen.clamp(0, totalLen - 1);
+
+ // Compute bubble positions
+ final nodes = [];
+ const bubbleRadius = 28.0; // 56x56
+ const eps = 0.001;
+
+ double d = startOffsetOnPath;
+ Offset? lastNodePosition; // Track last node position for finish flag
+
+ for (int i = 0; i < totalLevels; i++) {
+ final clamped = d.clamp(0, totalLen - eps);
+ final tan = metric.getTangentForOffset(clamped.toDouble())!;
+ final pos = tan.position;
+
+ // Store last node position
+ if (i == totalLevels - 1) {
+ lastNodePosition = pos;
+ }
+
+ nodes.add(Positioned(
+ left: pos.dx - bubbleRadius,
+ top: pos.dy - bubbleRadius,
+ child: LevelNode(
+ level: i + 1,
+ isCompleted: i < currentIndex,
+ isCurrent: i == currentIndex,
+ isLocked: i > currentIndex,
+ onTap: i > currentIndex ? null : () {
+ debugPrint('Level ${i + 1} tapped');
+ context.pushNamed('lesson');
},
- icon: Icon(
- themeMode == ThemeMode.dark
- ? Icons.light_mode
- : themeMode == ThemeMode.light
- ? Icons.dark_mode
- : Icons.brightness_auto,
- ),
- tooltip: 'Toggle theme',
),
- // Settings button
- IconButton(
- onPressed: () => context.push(RoutePaths.settings),
- icon: const Icon(Icons.settings),
- tooltip: 'Settings',
- ),
- ],
- ),
- body: SingleChildScrollView(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Welcome section
- _WelcomeCard(authState: authState),
+ ));
- const SizedBox(height: 24),
+ d += nodeSpacing;
+ if (d > totalLen - eps) break; // stop when path ends
+ }
- // Quick actions section
- Text(
- 'Quick Actions',
- style: Theme.of(context).textTheme.headlineSmall?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 16),
+ // Report current Y for auto-scroll
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (onCurrentLevelPositionResolved != null &&
+ currentIndex >= 0 &&
+ currentIndex < nodes.length) {
+ final clamped = (startOffsetOnPath + currentIndex * nodeSpacing)
+ .clamp(0, totalLen - eps);
+ final tan = metric.getTangentForOffset(clamped.toDouble())!;
+ onCurrentLevelPositionResolved!(tan.position.dy);
+ }
+ });
- _QuickActionsGrid(),
+ // Optional finish flag at last level position
+ if (showFinishAtEnd && lastNodePosition != null) {
+ nodes.add(Positioned(
+ left: lastNodePosition.dx - 18,
+ top: lastNodePosition.dy - 60,
+ child: const Icon(Icons.flag, color: Colors.red, size: 36),
+ ));
+ }
- const SizedBox(height: 24),
-
- // Features section
- Text(
- 'Features',
- style: Theme.of(context).textTheme.headlineSmall?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- ),
- const SizedBox(height: 16),
-
- _FeaturesGrid(),
-
- const SizedBox(height: 24),
-
- // Recent activity section
- _RecentActivityCard(),
- ],
- ),
- ),
- floatingActionButton: FloatingActionButton.extended(
- onPressed: () => context.push(RoutePaths.addTodo),
- icon: const Icon(Icons.add),
- label: const Text('Add Todo'),
- ),
- );
- }
-}
-
-class _WelcomeCard extends StatelessWidget {
- final AuthState authState;
-
- const _WelcomeCard({
- required this.authState,
- });
-
- @override
- Widget build(BuildContext context) {
- return Card(
- child: Padding(
- padding: const EdgeInsets.all(20.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- CircleAvatar(
- radius: 24,
- backgroundColor: Theme.of(context).colorScheme.primaryContainer,
- child: Icon(
- Icons.person,
- color: Theme.of(context).colorScheme.onPrimaryContainer,
- ),
- ),
- const SizedBox(width: 16),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- authState == AuthState.authenticated
- ? 'Welcome back!'
- : 'Welcome!',
- style: Theme.of(context).textTheme.headlineSmall?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- ),
- Text(
- authState == AuthState.authenticated
- ? 'Ready to be productive today?'
- : 'Get started with the base Flutter app.',
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- if (authState == AuthState.unauthenticated) ...[
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: OutlinedButton(
- onPressed: () => context.push(RoutePaths.login),
- child: const Text('Login'),
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: FilledButton(
- onPressed: () => context.push(RoutePaths.register),
- child: const Text('Register'),
- ),
- ),
- ],
- ),
- ],
- ],
- ),
- ),
- );
- }
-}
-
-class _QuickActionsGrid extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- final quickActions = [
- _QuickAction(
- icon: Icons.add_task,
- title: 'Add Todo',
- subtitle: 'Create a new task',
- onTap: () => context.push(RoutePaths.addTodo),
- ),
- _QuickAction(
- icon: Icons.list,
- title: 'View Todos',
- subtitle: 'See all tasks',
- onTap: () => context.push(RoutePaths.todos),
- ),
- _QuickAction(
- icon: Icons.settings,
- title: 'Settings',
- subtitle: 'App preferences',
- onTap: () => context.push(RoutePaths.settings),
- ),
- _QuickAction(
- icon: Icons.info,
- title: 'About',
- subtitle: 'App information',
- onTap: () => context.push(RoutePaths.about),
- ),
- ];
-
- return GridView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 2,
- crossAxisSpacing: 12,
- mainAxisSpacing: 12,
- childAspectRatio: 1.2,
- ),
- itemCount: quickActions.length,
- itemBuilder: (context, index) {
- return _QuickActionCard(action: quickActions[index]);
- },
- );
- }
-}
-
-class _FeaturesGrid extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- final features = [
- _Feature(
- icon: Icons.check_circle_outline,
- title: 'Todo Management',
- description: 'Create, edit, and track your tasks efficiently.',
- color: Colors.blue,
- onTap: () => context.push(RoutePaths.todos),
- ),
- _Feature(
- icon: Icons.palette,
- title: 'Theming',
- description: 'Switch between light, dark, and system themes.',
- color: Colors.purple,
- onTap: () => context.push(RoutePaths.settingsTheme),
- ),
- _Feature(
- icon: Icons.storage,
- title: 'Local Storage',
- description: 'Hive database integration for offline support.',
- color: Colors.orange,
- onTap: () => context.push(RoutePaths.settings),
- ),
- _Feature(
- icon: Icons.security,
- title: 'Secure Storage',
- description: 'Protected storage for sensitive data.',
- color: Colors.green,
- onTap: () => context.push(RoutePaths.settingsPrivacy),
- ),
- ];
-
- return GridView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 2,
- crossAxisSpacing: 12,
- mainAxisSpacing: 12,
- childAspectRatio: 0.8,
- ),
- itemCount: features.length,
- itemBuilder: (context, index) {
- return _FeatureCard(feature: features[index]);
- },
- );
- }
-}
-
-class _RecentActivityCard extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return Card(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- 'Recent Activity',
- style: Theme.of(context).textTheme.titleMedium?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- ),
- TextButton(
- onPressed: () => context.push(RoutePaths.todos),
- child: const Text('View All'),
- ),
- ],
- ),
- const SizedBox(height: 12),
- _ActivityItem(
- icon: Icons.add_task,
- title: 'Welcome to Base Flutter App',
- subtitle: 'Your journey starts here',
- time: 'Just now',
- ),
- _ActivityItem(
- icon: Icons.info,
- title: 'App initialized',
- subtitle: 'Database and services ready',
- time: 'A few seconds ago',
- ),
- ],
- ),
- ),
- );
- }
-}
-
-class _QuickActionCard extends StatelessWidget {
- final _QuickAction action;
-
- const _QuickActionCard({
- required this.action,
- });
-
- @override
- Widget build(BuildContext context) {
- return Card(
- child: InkWell(
- onTap: action.onTap,
- borderRadius: BorderRadius.circular(12),
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- action.icon,
- size: 32,
- color: Theme.of(context).colorScheme.primary,
- ),
- const SizedBox(height: 8),
- Text(
- action.title,
- style: Theme.of(context).textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 4),
- Text(
- action.subtitle,
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
- ),
- textAlign: TextAlign.center,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
-
-class _FeatureCard extends StatelessWidget {
- final _Feature feature;
-
- const _FeatureCard({
- required this.feature,
- });
-
- @override
- Widget build(BuildContext context) {
- return Card(
- child: InkWell(
- onTap: feature.onTap,
- borderRadius: BorderRadius.circular(12),
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: feature.color.withOpacity(0.1),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Icon(
- feature.icon,
- color: feature.color,
- size: 24,
- ),
- ),
- const SizedBox(height: 12),
- Text(
- feature.title,
- style: Theme.of(context).textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- const SizedBox(height: 4),
- Expanded(
- child: Text(
- feature.description,
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
- ),
- maxLines: 3,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
-
-class _ActivityItem extends StatelessWidget {
- final IconData icon;
- final String title;
- final String subtitle;
- final String time;
-
- const _ActivityItem({
- required this.icon,
- required this.title,
- required this.subtitle,
- required this.time,
- });
-
- @override
- Widget build(BuildContext context) {
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Row(
+ return Stack(
+ clipBehavior: Clip.none,
children: [
- Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Icon(
- icon,
- size: 16,
- color: Theme.of(context).colorScheme.onPrimaryContainer,
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- title,
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- fontWeight: FontWeight.w500,
- ),
- ),
- Text(
- subtitle,
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
- ),
- ),
- ],
- ),
- ),
- Text(
- time,
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
- ),
- ),
+ // Cute background bits (optional)
+ Positioned.fill(child: CustomPaint(painter: DecorationPainter())),
+ // Sandy road
+ Positioned.fill(child: CustomPaint(painter: SandPathPainter(path))),
+ // Bubbles
+ ...nodes,
],
- ),
- );
+ );
+ });
+ }
+
+ /// Creates a circular/spiral path that goes in circles while progressing downward
+ Path _buildCircularPath(Size size, double verticalStep) {
+ final path = Path();
+
+ final centerX = size.width * 0.5;
+ final baseRadius = size.width * 0.32; // radius of the circular motion
+
+ double y = 100;
+
+ // Start at top center
+ path.moveTo(centerX, y);
+
+ // Create circular loops going downward
+ while (y < size.height - 100) {
+ // Create a full circle using cubic bezier curves (4 curves make a circle)
+ final startY = y;
+
+ // Right curve (0° to 90°)
+ path.cubicTo(
+ centerX + baseRadius * 0.55, startY,
+ centerX + baseRadius, startY + verticalStep * 0.25 - baseRadius * 0.55,
+ centerX + baseRadius, startY + verticalStep * 0.25,
+ );
+
+ // Bottom curve (90° to 180°)
+ path.cubicTo(
+ centerX + baseRadius, startY + verticalStep * 0.25 + baseRadius * 0.55,
+ centerX + baseRadius * 0.55, startY + verticalStep * 0.5,
+ centerX, startY + verticalStep * 0.5,
+ );
+
+ // Left curve (180° to 270°)
+ path.cubicTo(
+ centerX - baseRadius * 0.55, startY + verticalStep * 0.5,
+ centerX - baseRadius, startY + verticalStep * 0.75 - baseRadius * 0.55,
+ centerX - baseRadius, startY + verticalStep * 0.75,
+ );
+
+ // Top curve (270° to 360°)
+ path.cubicTo(
+ centerX - baseRadius, startY + verticalStep * 0.75 + baseRadius * 0.55,
+ centerX - baseRadius * 0.55, startY + verticalStep,
+ centerX, startY + verticalStep,
+ );
+
+ y += verticalStep;
+ }
+
+ return path;
}
}
-class _QuickAction {
- final IconData icon;
- final String title;
- final String subtitle;
- final VoidCallback onTap;
+/// Painter for the sand road with subtle depth.
+class SandPathPainter extends CustomPainter {
+ final Path path;
+ SandPathPainter(this.path);
- const _QuickAction({
- required this.icon,
- required this.title,
- required this.subtitle,
- required this.onTap,
- });
+ @override
+ void paint(Canvas canvas, Size size) {
+ // Glow/soft highlight
+ final glow = Paint()
+ ..color = const Color(0x26FFFFFF)
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 84
+ ..strokeCap = StrokeCap.round
+ ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10);
+ canvas.drawPath(path, glow);
+
+ // Main sand
+ final sand = Paint()
+ ..color = const Color(0xFFFFD18C)
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 64
+ ..strokeCap = StrokeCap.round
+ ..strokeJoin = StrokeJoin.round;
+ canvas.drawPath(path, sand);
+
+ // Inner shade for depth
+ final inner = Paint()
+ ..color = const Color(0xFFEAB66F)
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 36
+ ..strokeCap = StrokeCap.round;
+ canvas.drawPath(path, inner);
+ }
+
+ @override
+ bool shouldRepaint(covariant SandPathPainter oldDelegate) =>
+ oldDelegate.path != path;
}
-class _Feature {
- final IconData icon;
- final String title;
- final String description;
- final Color color;
- final VoidCallback onTap;
+/// Simple decorative painter (palms/hills placeholders).
+class DecorationPainter extends CustomPainter {
+ @override
+ void paint(Canvas canvas, Size size) {
+ final palm = Paint()..color = const Color(0xFF66A060);
+ final hill = Paint()..color = const Color(0xFFFFE1A8).withOpacity(.35);
- const _Feature({
- required this.icon,
- required this.title,
- required this.description,
- required this.color,
- required this.onTap,
+ for (double y = 160; y < size.height; y += 260) {
+ canvas.drawCircle(Offset(size.width * 0.18, y), 10, palm);
+ canvas.drawCircle(Offset(size.width * 0.82, y + 70), 10, palm);
+ canvas.drawRRect(
+ RRect.fromRectAndRadius(
+ Rect.fromCenter(
+ center: Offset(size.width * 0.5, y + 120),
+ width: 140,
+ height: 60,
+ ),
+ const Radius.circular(30),
+ ),
+ hill,
+ );
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}
+
+class LevelNode extends StatelessWidget {
+ final int level;
+ final bool isCompleted;
+ final bool isCurrent;
+ final bool isLocked;
+ final VoidCallback? onTap;
+
+ const LevelNode({
+ super.key,
+ required this.level,
+ required this.isCompleted,
+ required this.isCurrent,
+ required this.isLocked,
+ this.onTap,
});
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: isLocked ? null : onTap,
+ child: Container(
+ width: 56,
+ height: 56,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.white,
+ border: Border.all(
+ color: isCurrent ? Colors.orange : Colors.white,
+ width: 4,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.18),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Center(child: _inner()),
+ ),
+ );
+ }
+
+ Widget _inner() {
+ if (isCompleted) {
+ return const Icon(Icons.check_circle, color: Colors.green, size: 28);
+ }
+ if (isLocked) {
+ return const Icon(Icons.lock, color: Colors.grey, size: 22);
+ }
+ if (isCurrent) {
+ return Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: LinearGradient(
+ colors: [Colors.orange.shade300, Colors.orange.shade700],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ ),
+ child: const Icon(Icons.play_arrow, color: Colors.white),
+ );
+ }
+ return Text(
+ '$level',
+ style: const TextStyle(
+ fontSize: 18, fontWeight: FontWeight.bold, color: Colors.orange),
+ );
+ }
}
\ No newline at end of file
diff --git a/lib/features/lesson/lesson_screen.dart b/lib/features/lesson/lesson_screen.dart
new file mode 100644
index 0000000..0d054a5
--- /dev/null
+++ b/lib/features/lesson/lesson_screen.dart
@@ -0,0 +1,113 @@
+import 'dart:convert';
+import 'package:base_flutter/features/lesson/record_dialog.dart';
+import 'package:record/record.dart';
+
+import 'package:base_flutter/features/home/models.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_tts/flutter_tts.dart';
+
+FlutterTts flutterTts = FlutterTts();
+
+enum PositionEnum {
+ topLeft('top_left'),
+ topRight('top_right'),
+ bottomLeft('bottom_left'),
+ bottomRight('bottom_right');
+
+ final String value;
+ const PositionEnum(this.value);
+}
+
+class LessonScreen extends StatefulWidget {
+ const LessonScreen({super.key, required this.lessonResponse});
+
+ final LessonResponse lessonResponse;
+
+ @override
+ State createState() => _LessonScreenState();
+}
+
+class _LessonScreenState extends State {
+ @override
+ Widget build(BuildContext context) {
+
+ final record = AudioRecorder();
+
+
+
+ Widget containerText(String text) {
+ return InkWell(
+ onTap: () async {
+ var result = await flutterTts.speak(text);
+ },
+ child: Container(
+ padding: const EdgeInsets.all(8.0),
+ margin: const EdgeInsets.all(16.0),
+ width: 300,
+ height: 120,
+ color: Colors.black54,
+ child: Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
+ ),
+ );
+ }
+
+ Widget buildTapToRecordWidget(String position, String content) {
+ switch (position) {
+ case 'top_left':
+ return Align(alignment: Alignment.topLeft, child: containerText(content));
+ case 'top_right':
+ return Align(alignment: Alignment.topRight, child: containerText(content));
+ case 'bottom_left':
+ return Align(alignment: Alignment.bottomLeft, child: containerText(content));
+ case 'bottom_right':
+ return Align(alignment: Alignment.bottomRight, child: containerText(content));
+ default:
+ return const SizedBox.shrink();
+ }
+ }
+
+ return Scaffold(
+ extendBodyBehindAppBar: true,
+ appBar: AppBar(
+ backgroundColor: Colors.transparent,
+ elevation: 0,
+
+ ),
+ body: Container(
+ decoration: BoxDecoration(
+ image: DecorationImage(
+ image: MemoryImage(base64Decode(widget.lessonResponse.image)),
+ fit: BoxFit.cover, // Optional: Adjust as needed
+ ),
+ ),
+ child: Column(
+ children: [
+ SizedBox(height: kToolbarHeight,),
+ Text(widget.lessonResponse.widget.tapToRecordWidget.position),
+ buildTapToRecordWidget(
+ widget.lessonResponse.widget.tapToRecordWidget.position,
+ widget.lessonResponse.widget.tapToRecordWidget.content,
+ ),
+ Spacer(),
+ Text(widget.lessonResponse.widget.tapToSpeechWidget.position),
+ buildTapToRecordWidget(
+ widget.lessonResponse.widget.tapToSpeechWidget.position,
+ widget.lessonResponse.widget.tapToSpeechWidget.content,
+ ),
+ IconButton(
+ onPressed: () {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ content: RecordDialog(),
+ ),
+ );
+ },
+ icon: Icon(Icons.mic, size: 48),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/lesson/record_dialog.dart b/lib/features/lesson/record_dialog.dart
new file mode 100644
index 0000000..ede40bf
--- /dev/null
+++ b/lib/features/lesson/record_dialog.dart
@@ -0,0 +1,79 @@
+import 'package:flutter/material.dart';
+import 'package:record/record.dart';
+import 'package:audioplayers/audioplayers.dart';
+
+class RecordDialog extends StatefulWidget {
+ const RecordDialog({super.key});
+
+ @override
+ State createState() => _RecordDialogState();
+}
+
+class _RecordDialogState extends State {
+ final AudioRecorder _recorder = AudioRecorder();
+ final AudioPlayer _player = AudioPlayer();
+ String? _recordedPath;
+ bool _isRecording = false;
+
+ @override
+ void dispose() {
+ _recorder.dispose();
+ _player.dispose();
+ super.dispose();
+ }
+
+ Future _toggleRecord() async {
+ if (!_isRecording) {
+ if (await _recorder.hasPermission()) {
+ await _recorder.start(const RecordConfig(), path: 'myFile.m4a');
+ setState(() => _isRecording = true);
+ }
+ } else {
+ final path = await _recorder.stop();
+ setState(() {
+ _isRecording = false;
+ _recordedPath = path;
+ });
+ }
+ }
+
+ Future _playRecording() async {
+ if (_recordedPath != null) {
+ await _player.play(DeviceFileSource(_recordedPath!));
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: 300,
+ height: 400,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const SizedBox(height: 150),
+ IconButton(
+ icon: Icon(_isRecording ? Icons.stop : Icons.mic_none_sharp, size: 96),
+ onPressed: _toggleRecord,
+ ),
+ if (_recordedPath != null && !_isRecording)
+ IconButton(
+ icon: const Icon(Icons.play_arrow, size: 48),
+ onPressed: _playRecording,
+ ),
+ const Spacer(),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Close')),
+ // Add more buttons as needed
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// Usage in your code:
+
diff --git a/lib/features/loading_lesson/loading_lesson_screen.dart b/lib/features/loading_lesson/loading_lesson_screen.dart
new file mode 100644
index 0000000..dbcabb4
--- /dev/null
+++ b/lib/features/loading_lesson/loading_lesson_screen.dart
@@ -0,0 +1,52 @@
+import 'package:base_flutter/features/home/models.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+class LoadingLessonScreen extends StatefulWidget {
+ const LoadingLessonScreen({super.key});
+
+ @override
+ State createState() => _LoadingLessonScreenState();
+}
+
+class _LoadingLessonScreenState extends State {
+
+ LessonResponse? lessonResponse;
+
+
+ @override
+ void initState() {
+ // TODO: implement initState
+ super.initState();
+ callDio();
+ }
+
+
+ void callDio() async {
+ final dio = Dio();
+ final response = await dio.get('https://37b530e059dd.ngrok-free.app/');
+ setState(() {
+ lessonResponse = LessonResponse.fromJson(response.data);
+ });
+ }
+
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(),
+ body: Center(
+ child: lessonResponse == null ? const CircularProgressIndicator() : Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text('Lesson Title: ${lessonResponse!.response}'),
+ TextButton(onPressed: () {
+ context.pushNamed('test', extra: lessonResponse);
+ }, child: Text('Next')),
+ ],
+ )
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index b4ceb4a..75875f9 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -41,6 +41,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
+ audioplayers:
+ dependency: "direct main"
+ description:
+ name: audioplayers
+ sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.5.1"
+ audioplayers_android:
+ dependency: transitive
+ description:
+ name: audioplayers_android
+ sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.2.1"
+ audioplayers_darwin:
+ dependency: transitive
+ description:
+ name: audioplayers_darwin
+ sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.0"
+ audioplayers_linux:
+ dependency: transitive
+ description:
+ name: audioplayers_linux
+ sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.1"
+ audioplayers_platform_interface:
+ dependency: transitive
+ description:
+ name: audioplayers_platform_interface
+ sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.1.1"
+ audioplayers_web:
+ dependency: transitive
+ description:
+ name: audioplayers_web
+ sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.1"
+ audioplayers_windows:
+ dependency: transitive
+ description:
+ name: audioplayers_windows
+ sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.1"
boolean_selector:
dependency: transitive
description:
@@ -379,6 +435,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_tts:
+ dependency: "direct main"
+ description:
+ name: flutter_tts
+ sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.3"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -728,6 +792,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
+ record:
+ dependency: "direct main"
+ description:
+ name: record
+ sha256: "9dbc6ff3e784612f90a9b001373c45ff76b7a08abd2bd9fdf72c242320c8911c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.1"
+ record_android:
+ dependency: transitive
+ description:
+ name: record_android
+ sha256: "854627cd78d8d66190377f98477eee06ca96ab7c9f2e662700daf33dbf7e6673"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.2"
+ record_ios:
+ dependency: transitive
+ description:
+ name: record_ios
+ sha256: "13e241ed9cbc220534a40ae6b66222e21288db364d96dd66fb762ebd3cb77c71"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ record_linux:
+ dependency: transitive
+ description:
+ name: record_linux
+ sha256: "235b1f1fb84e810f8149cc0c2c731d7d697f8d1c333b32cb820c449bf7bb72d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ record_macos:
+ dependency: transitive
+ description:
+ name: record_macos
+ sha256: "2849068bb59072f300ad63ed146e543d66afaef8263edba4de4834fc7c8d4d35"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ record_platform_interface:
+ dependency: transitive
+ description:
+ name: record_platform_interface
+ sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ record_web:
+ dependency: transitive
+ description:
+ name: record_web
+ sha256: "4f0adf20c9ccafcc02d71111fd91fba1ca7b17a7453902593e5a9b25b74a5c56"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ record_windows:
+ dependency: transitive
+ description:
+ name: record_windows
+ sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.7"
riverpod:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 5fb3408..22d7e74 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -63,6 +63,11 @@ dependencies:
# Image Caching
cached_network_image: ^3.3.1
+
+ flutter_tts: ^4.2.3
+ record: ^6.1.1
+ audioplayers: ^6.5.1
+
dev_dependencies:
flutter_test:
sdk: flutter