diff --git a/lib/config/router.dart b/lib/config/router.dart index 4a0ed6e..247c8ef 100644 --- a/lib/config/router.dart +++ b/lib/config/router.dart @@ -9,10 +9,10 @@ import 'package:lacquer/presentation/pages/auth/login_page.dart'; import 'package:lacquer/presentation/pages/auth/verify_page.dart'; import 'package:lacquer/presentation/pages/camera/camera_page.dart'; import 'package:lacquer/presentation/pages/camera/about_screen.dart'; -import 'package:lacquer/services/ai_service.dart'; import 'package:lacquer/presentation/pages/home/add_new_word_page.dart'; import 'package:lacquer/presentation/pages/home/edit_card_list_page.dart'; import 'package:lacquer/presentation/pages/home/dictionary_page.dart'; +import 'package:lacquer/presentation/pages/home/revise_flashcard_page.dart'; import 'package:lacquer/presentation/pages/home/quiz_page.dart'; import 'package:lacquer/presentation/pages/profile/profile_page.dart'; import 'package:lacquer/presentation/pages/home/flashcard_page.dart'; @@ -21,8 +21,6 @@ import 'package:lacquer/presentation/pages/friends/friends_page.dart'; import 'package:lacquer/features/profile/bloc/profile_bloc.dart'; import 'package:lacquer/features/profile/bloc/profile_event.dart'; import 'package:lacquer/presentation/pages/home/translator_page.dart'; -import 'package:lacquer/presentation/pages/chat/chat_screen.dart'; -import 'package:lacquer/presentation/pages/profile/badge_collection_page_simple.dart'; import 'package:lacquer/presentation/pages/mainscreen.dart'; import 'package:flutter/widgets.dart'; @@ -38,14 +36,13 @@ class RouteName { static const String profile = '/profile'; static const String flashcards = '/flashcards'; static String learn(String deckId) => '/learn/$deckId'; + static String revise(String deckId) => '/revise/$deckId'; static String edit(String deckId) => '/edit/$deckId'; static const String dictionary = '/dictionary'; static const String translator = '/translator'; static const String friends = '/friends'; static const String quiz = '/quiz'; static String addNewWord(String deckId) => '/add-new-word/$deckId'; - static const String chat = '/chat'; - static const String badges = '/badges'; static const publicRoutes = [login, forgotPassword, verify, register]; } @@ -103,10 +100,8 @@ final router = GoRouter( noTransitionRoute( path: RouteName.about, builder: (context, state) { - final extra = state.extra as Map; - final imagePath = extra['imagePath'] as String; - final aiResult = extra['aiResult'] as AIResult?; - return AboutScreen(imagePath: imagePath, aiResult: aiResult); + final imagePath = state.extra as String; + return AboutScreen(imagePath: imagePath); }, ), noTransitionRoute( @@ -136,6 +131,17 @@ final router = GoRouter( return LearningFlashcardPage(deckId: deckId); }, ), + noTransitionRoute( + path: '/revise/:deckId', + builder: (context, state) { + final deckId = state.pathParameters['deckId']!; + return ReviseFlashcardPage(deckId: deckId); + }, + ), + noTransitionRoute( + path: RouteName.translator, + builder: (context, state) => const TranslatorScreen(), + ), noTransitionRoute( path: RouteName.translator, builder: (context, state) => const TranslatorScreen(), @@ -154,13 +160,5 @@ final router = GoRouter( return AddNewWordPage(deckId: deckId); }, ), - noTransitionRoute( - path: RouteName.chat, - builder: (context, state) => const ChatScreen(), - ), - noTransitionRoute( - path: RouteName.badges, - builder: (context, state) => const BadgeCollectionPage(), - ), ], -); \ No newline at end of file +); diff --git a/lib/presentation/pages/home/flashcard_page.dart b/lib/presentation/pages/home/flashcard_page.dart index c13992b..102a224 100644 --- a/lib/presentation/pages/home/flashcard_page.dart +++ b/lib/presentation/pages/home/flashcard_page.dart @@ -51,84 +51,100 @@ class _FlashcardPageState extends State { @override Widget build(BuildContext context) { - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, result) async { - if (didPop) return; - context.go('/'); - }, - child: Scaffold( - backgroundColor: CustomTheme.lightbeige, - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAppBar(context), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: _buildSearchBar(), - ), - BlocBuilder( - builder: (context, state) { - if (state.status == FlashcardStatus.loading) { - return const Padding( - padding: EdgeInsets.only(top: 16.0), - child: Center(child: CircularProgressIndicator()), - ); - } else if (state.status == FlashcardStatus.failure) { - return Center( - child: Text( - 'Error: ${state.errorMessage ?? 'Unknown error'}', - ), - ); - } else if (state.status == FlashcardStatus.success) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!state.searchResult) - SizedBox( - height: 30, - child: Center( - child: Text( - 'No result', - style: TextStyle( - fontSize: 18, - color: Colors.grey.shade600, + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + CustomTheme.loginGradientStart, + CustomTheme.loginGradientEnd, + ], + begin: FractionalOffset(0.5, 0.0), + end: FractionalOffset(0.5, 1.0), + stops: [0.0, 1.0], + tileMode: TileMode.clamp, + ), + ), + child: PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + context.go('/'); + }, + child: Scaffold( + backgroundColor: Colors.transparent, + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAppBar(context), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: _buildSearchBar(), + ), + BlocBuilder( + builder: (context, state) { + if (state.status == FlashcardStatus.loading) { + return const Padding( + padding: EdgeInsets.only(top: 16.0), + child: Center(child: CircularProgressIndicator()), + ); + } else if (state.status == FlashcardStatus.failure) { + return Center( + child: Text( + 'Error: ${state.errorMessage ?? 'Unknown error'}', + ), + ); + } else if (state.status == FlashcardStatus.success) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!state.searchResult) + SizedBox( + height: 30, + child: Center( + child: Text( + 'No result', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade600, + ), ), ), ), - ), - if (state.groupedDecks != null && - state.groupedDecks!.data.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: - state.groupedDecks!.data.map((group) { - return group.decks.isNotEmpty - ? FlashcardTag( - key: ValueKey(group.tag.id), - tagId: group.tag.id, - title: group.tag.name, - decks: group.decks, - ) - : const SizedBox.shrink(); - }).toList(), - ) - else if (state.groupedDecks == null) - const Padding( - padding: EdgeInsets.only(left: 16.0, top: 8.0), - child: Text('No decks available'), - ), - ], - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - const SizedBox(height: 20), - ], + if (state.groupedDecks != null && + state.groupedDecks!.data.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + state.groupedDecks!.data.map((group) { + return group.decks.isNotEmpty + ? FlashcardTag( + key: ValueKey(group.tag.id), + tagId: group.tag.id, + title: group.tag.name, + decks: group.decks, + ) + : const SizedBox.shrink(); + }).toList(), + ) + else if (state.groupedDecks == null) + const Padding( + padding: EdgeInsets.only(left: 16.0, top: 8.0), + child: Text('No decks available'), + ), + ], + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + const SizedBox(height: 20), + ], + ), ), ), ), diff --git a/lib/presentation/pages/home/revise_flashcard_page.dart b/lib/presentation/pages/home/revise_flashcard_page.dart new file mode 100644 index 0000000..04fd49c --- /dev/null +++ b/lib/presentation/pages/home/revise_flashcard_page.dart @@ -0,0 +1,264 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lacquer/config/router.dart'; +import 'package:lacquer/config/theme.dart'; +import 'package:lacquer/features/flashcard/bloc/flashcard_bloc.dart'; +import 'package:lacquer/features/flashcard/bloc/flashcard_event.dart'; +import 'package:lacquer/features/flashcard/bloc/flashcard_state.dart'; +import 'package:lacquer/presentation/pages/home/widgets/revise_card_list.dart'; +import 'package:lacquer/presentation/pages/home/widgets/revise_instruction_dialog.dart'; +import 'package:lacquer/presentation/pages/home/widgets/speech_adjustment.dart'; + +class ReviseFlashcardPage extends StatefulWidget { + final String deckId; + const ReviseFlashcardPage({super.key, required this.deckId}); + @override + State createState() => _ReviseFlashcardPageState(); +} + +class _ReviseFlashcardPageState extends State { + double _progress = 0.0; + double _speechRate = 0.5; + String _selectedAccent = 'en-US'; + @override + void initState() { + super.initState(); + context.read().add(LoadDeckByIdRequested(widget.deckId)); + } + + void _updateProgress(double progress) { + setState(() { + _progress = progress.clamp(0.0, 1.0); + }); + } + + void _showTtsSettings() async { + final result = await showDialog>( + context: context, + builder: + (context) => SpeechAdjustment( + initialSpeed: _speechRate, + initialAccent: + _accents.entries + .firstWhere( + (entry) => entry.value == _selectedAccent, + orElse: () => const MapEntry('US English', 'en-US'), + ) + .key, + ), + ); + if (result != null) { + setState(() { + _speechRate = result['speed'] as double; + _selectedAccent = result['accent'] as String; + }); + } + } + + static final Map _accents = { + 'US English': 'en-US', + 'UK English': 'en-GB', + 'Australian': 'en-AU', + 'Indian': 'en-IN', + }; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: CustomTheme.lightbeige, + body: BlocBuilder( + builder: (context, state) { + if (state.status == FlashcardStatus.loading) { + return const Center(child: CircularProgressIndicator()); + } else if (state.status == FlashcardStatus.failure) { + return Center( + child: Text('Error: ${state.errorMessage ?? 'Unknown error'}'), + ); + } else if (state.status == FlashcardStatus.success && + state.selectedDeck != null) { + final deck = state.selectedDeck!; + return Stack( + children: [ + _buildAppBar(context, deck.title), + Column( + children: [ + const SizedBox(height: 100), + if (deck.cards != null && deck.cards!.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 200, + child: ClipRRect( + borderRadius: BorderRadius.circular(4.0), + child: LinearProgressIndicator( + value: _progress, + backgroundColor: Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + const Color.fromARGB(255, 104, 175, 106), + ), + minHeight: 12.0, + ), + ), + ), + const SizedBox(width: 12), + Text( + '${(_progress * 100).toStringAsFixed(0)}%', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + Expanded( + child: + (deck.cards == null || deck.cards!.isEmpty) + ? Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.menu_book_outlined, + size: 80, + color: Colors.grey, + ), + const SizedBox(height: 16), + const Text( + 'No cards yet', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black54, + ), + ), + const SizedBox(height: 8), + const Text( + 'Start by adding your first card to this deck.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.black45, + ), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: + () => context.go( + RouteName.addNewWord( + widget.deckId, + ), + ), + icon: const Icon( + Icons.add, + color: Colors.white, + ), + label: const Text('Add New Card'), + style: ElevatedButton.styleFrom( + backgroundColor: + CustomTheme.mainColor1, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8, + ), + ), + ), + ), + ], + ), + ), + ) + : ReviseCardList( + deckId: widget.deckId, + cards: deck.cards!, + onScrollProgress: _updateProgress, + speechRate: _speechRate, + selectedAccent: _selectedAccent, + ), + ), + ], + ), + ], + ); + } + return const Center(child: Text('No deck data available')); + }, + ), + ); + } + + Widget _buildAppBar(BuildContext context, String? title) { + return ClipRRect( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + child: Container( + height: 90, + color: CustomTheme.mainColor1, + padding: const EdgeInsets.only(top: 30), + child: Center( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(width: 10), + IconButton( + icon: const Icon( + FontAwesomeIcons.arrowLeft, + color: Colors.white, + ), + onPressed: () { + context.go(RouteName.flashcards); + }, + ), + Text( + title ?? '', + style: const TextStyle( + fontSize: 24, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 15), + const Spacer(), + IconButton( + icon: const Icon(FontAwesomeIcons.gear, color: Colors.white), + onPressed: _showTtsSettings, + ), + IconButton( + icon: const Icon( + FontAwesomeIcons.question, + color: Colors.white, + ), + onPressed: () { + showDialog( + context: context, + builder: (context) => const ReviseCardInstructionsDialog(), + ); + }, + ), + const SizedBox(width: 10), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/flashcard_options.dart b/lib/presentation/pages/home/widgets/flashcard_options.dart index 30d42a9..ba6e36b 100644 --- a/lib/presentation/pages/home/widgets/flashcard_options.dart +++ b/lib/presentation/pages/home/widgets/flashcard_options.dart @@ -48,7 +48,7 @@ class _FlashcardOptionDialogState extends State { "title": "Revise", "subtitle": "", "action": () { - print("Revise clicked"); + context.go(RouteName.revise(widget.id)); }, }, { diff --git a/lib/presentation/pages/home/widgets/revise_card.dart b/lib/presentation/pages/home/widgets/revise_card.dart new file mode 100644 index 0000000..cbaf99e --- /dev/null +++ b/lib/presentation/pages/home/widgets/revise_card.dart @@ -0,0 +1,262 @@ +import 'dart:async'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_tts/flutter_tts.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:lacquer/config/theme.dart'; +import 'package:lacquer/features/flashcard/dtos/card_dto.dart'; +import 'package:lacquer/presentation/utils/wave_clipper.dart'; + +class ReviseCard extends StatefulWidget { + final CardDto card; + final double speechRate; + final String selectedAccent; + + const ReviseCard({ + super.key, + required this.card, + required this.speechRate, + required this.selectedAccent, + }); + + @override + State createState() => _ReviseCardState(); +} + +class _ReviseCardState extends State { + late FlutterTts flutterTts; + bool _showMeaning = false; + int _countdown = 5; + Timer? _timer; + + @override + void initState() { + super.initState(); + + flutterTts = FlutterTts(); + _initializeTts(); + + _startCountdownTimer(); + } + + void _startCountdownTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_countdown <= 1) { + timer.cancel(); + if (mounted) { + setState(() { + _countdown = 0; + _showMeaning = true; + }); + } + } else { + if (mounted) { + setState(() { + _countdown--; + }); + } + } + }); + } + + Future _initializeTts() async { + await flutterTts.setLanguage(widget.selectedAccent); + await flutterTts.setSpeechRate(widget.speechRate); + await flutterTts.setPitch(1.0); + } + + Future _speak(String text) async { + await flutterTts.setLanguage(widget.selectedAccent); + await flutterTts.setSpeechRate(widget.speechRate); + await flutterTts.setPitch(1.0); + await flutterTts.speak(text); + } + + @override + void dispose() { + flutterTts.stop(); + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 0), + child: Container( + width: size.width - 50, + height: size.height - 180, + decoration: BoxDecoration( + color: CustomTheme.flashcardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: const Color.fromRGBO(0, 0, 0, 0.2), + blurRadius: 10, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + children: [ + _buildTop(card: widget.card, size: size), + _buildMeaning(card: widget.card), + ], + ), + ), + ), + ); + } + + Widget _buildTop({required CardDto card, required Size size}) { + return ClipPath( + clipper: WaveClipper(), + child: Container( + height: (size.height - 180) / 2, + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Stack( + children: [ + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + card.word ?? '', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + if ((card.pronunciation ?? '').isNotEmpty) ...[ + const SizedBox(height: 12), + Text( + '[${card.pronunciation}]', + style: const TextStyle( + fontSize: 20, + fontStyle: FontStyle.italic, + color: Colors.grey, + ), + ), + ], + ], + ), + ), + Positioned( + bottom: 24, + right: 12, + child: IconButton( + onPressed: () { + if ((card.word ?? '').isNotEmpty) { + _speak(card.word!); + } + }, + icon: const Icon( + FontAwesomeIcons.volumeHigh, + color: Colors.grey, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildMeaning({required CardDto card}) { + final blurValue = _showMeaning ? 0.0 : 10.0; + + return Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: CustomTheme.flashcardColor, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(16), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Column( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (card.meaning?.type?.isNotEmpty ?? false) + ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: blurValue, + sigmaY: blurValue, + ), + child: Text( + card.meaning!.type!, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + if (card.meaning?.definition?.isNotEmpty ?? false) + ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: blurValue, + sigmaY: blurValue, + ), + child: Text( + card.meaning!.definition!, + style: const TextStyle( + fontSize: 20, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + + Align( + alignment: Alignment.bottomCenter, + child: ElevatedButton.icon( + onPressed: + _showMeaning + ? null + : () { + setState(() { + _showMeaning = true; + _timer?.cancel(); + }); + }, + icon: const Icon(Icons.visibility, color: Colors.white), + label: Text( + _showMeaning ? 'Shown' : 'Show (${_countdown}s)', + style: const TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 141, 188, 24), + padding: const EdgeInsets.symmetric( + horizontal: 28, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/revise_card_list.dart b/lib/presentation/pages/home/widgets/revise_card_list.dart new file mode 100644 index 0000000..162406e --- /dev/null +++ b/lib/presentation/pages/home/widgets/revise_card_list.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lacquer/config/router.dart'; +import 'package:lacquer/features/flashcard/dtos/card_dto.dart'; +import 'package:lacquer/presentation/pages/home/widgets/revise_card.dart'; +import 'dart:math'; + +class ReviseCardList extends StatefulWidget { + final String deckId; + final List cards; + final Function(double)? onScrollProgress; + final double speechRate; + final String selectedAccent; + + const ReviseCardList({ + super.key, + required this.deckId, + required this.cards, + this.onScrollProgress, + required this.speechRate, + required this.selectedAccent, + }); + + @override + State createState() => _ReviseCardListState(); +} + +class _ReviseCardListState extends State + with SingleTickerProviderStateMixin { + List _cardStack = []; + Offset _dragOffset = Offset.zero; + double _rotation = 0.0; + bool _isDragging = false; + + @override + void initState() { + super.initState(); + _cardStack = List.from(widget.cards); + } + + void _handleDragUpdate(DragUpdateDetails details) { + setState(() { + _dragOffset += details.delta; + _rotation = _dragOffset.dx / 300; + _isDragging = true; + }); + } + + void _handleDragEnd(DragEndDetails details) { + final threshold = 150; + final draggedRight = _dragOffset.dx > threshold; + final draggedLeft = _dragOffset.dx < -threshold; + + if (draggedRight || draggedLeft) { + final card = _cardStack.last; + setState(() { + _cardStack.removeLast(); + _dragOffset = Offset.zero; + _rotation = 0.0; + _isDragging = false; + }); + + if (draggedRight) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + _cardStack.insert(0, card); + }); + }); + } + + final initialCount = widget.cards.length; + final remainingCount = _cardStack.length; + final progress = + initialCount > 0 + ? (initialCount - remainingCount) / initialCount + : 0.0; + widget.onScrollProgress?.call(progress.clamp(0.0, 1.0)); + } else { + setState(() { + _dragOffset = Offset.zero; + _rotation = 0.0; + _isDragging = false; + }); + } + } + + @override + Widget build(BuildContext context) { + if (_cardStack.isEmpty) { + return _buildCompletionCard(context, widget.deckId); + } + + return Stack( + children: + List.generate(min(3, _cardStack.length), (index) { + final cardIndex = _cardStack.length - 1 - index; + final card = _cardStack[cardIndex]; + final isTopCard = index == 0; + + final offset = isTopCard ? _dragOffset : Offset.zero; + final angle = isTopCard ? _rotation : 0.0; + + final transform = Transform.translate( + offset: offset, + child: Transform.rotate( + angle: angle, + child: ReviseCard( + card: card, + speechRate: widget.speechRate, + selectedAccent: widget.selectedAccent, + ), + ), + ); + + return Positioned( + top: index * 10.0, + left: 0, + right: 0, + child: + isTopCard + ? Stack( + children: [ + if (_isDragging) + Positioned.fill( + child: Row( + children: [ + Expanded( + child: Container( + color: + _dragOffset.dx < -50 + ? Colors.red + : Colors.red, + child: Center( + child: Text( + 'Forget', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + + Expanded( + child: Container( + color: + _dragOffset.dx > 50 + ? Colors.green + : Colors.green, + child: Center( + child: Text( + 'Remember', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + GestureDetector( + onPanUpdate: _handleDragUpdate, + onPanEnd: _handleDragEnd, + child: transform, + ), + ], + ) + : Transform.scale( + scale: 1 - (index * 0.03), + child: transform, + ), + ); + }).reversed.toList(), + ); + } +} + +Widget _buildCompletionCard(BuildContext context, String deckId) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.emoji_events, size: 80, color: Colors.amber), + const SizedBox(height: 24), + const Text( + '🎉 Congratulations!', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Text( + 'You have completed the deck.', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () { + context.go(RouteName.flashcards); + }, + icon: const Icon(Icons.check), + label: const Text('Got it'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + backgroundColor: Colors.green, + textStyle: const TextStyle(fontSize: 16), + ), + ), + ], + ), + ), + ); +} diff --git a/lib/presentation/pages/home/widgets/revise_instruction_dialog.dart b/lib/presentation/pages/home/widgets/revise_instruction_dialog.dart new file mode 100644 index 0000000..5479b27 --- /dev/null +++ b/lib/presentation/pages/home/widgets/revise_instruction_dialog.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +class ReviseCardInstructionsDialog extends StatelessWidget { + const ReviseCardInstructionsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.info_outline, + size: 48, + color: Colors.amber, + semanticLabel: 'Instructions', + ), + const SizedBox(height: 16), + const Text( + 'How to Revise Flashcards', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Row( + children: [ + Column( + children: [ + Icon(FontAwesomeIcons.leftLong), + const SizedBox(height: 8), + Text( + 'Swipe Left to mark\n a card as known', + style: TextStyle(fontSize: 14), + ), + ], + ), + const Spacer(), + Column( + children: [ + Icon(FontAwesomeIcons.rightLong), + const SizedBox(height: 8), + Text( + 'Swipe Right to mark\n a card as forgotten', + style: TextStyle(fontSize: 14), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + Semantics( + button: true, + label: 'Close instructions', + child: ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.check, color: Colors.white), + label: const Text( + 'Got it', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + backgroundColor: Colors.green, + textStyle: const TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e097c26..0da8462 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" bloc: dependency: transitive description: @@ -250,10 +250,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -689,10 +689,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -1134,10 +1134,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" watcher: dependency: transitive description: