From 4352ecbec0954cd060ed6c1cb3ee0dd28a23e1f8 Mon Sep 17 00:00:00 2001 From: djanderson26 Date: Sat, 18 Apr 2026 19:01:01 -0400 Subject: [PATCH 1/3] feat(study): allow bidirectional card flipping and rating from either face (#128) - Update FlipCard to accept taps when already flipped - Change _flipCard() to toggle state instead of only enabling - Enable swipe-to-rate from both front and back faces (mobile) - Allow rating keyboard shortcuts (1-4) from either face - Make rating buttons always visible and clickable on desktop Users can now freely flip between front/back and rate from any face, improving the study session UX by removing the lock-to-back restriction. --- .../screens/study_session_screen.dart | 38 +++++++++++-------- .../study/presentation/widgets/flip_card.dart | 10 ++--- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/features/study/presentation/screens/study_session_screen.dart b/lib/features/study/presentation/screens/study_session_screen.dart index 3cd6105..ae8a44f 100644 --- a/lib/features/study/presentation/screens/study_session_screen.dart +++ b/lib/features/study/presentation/screens/study_session_screen.dart @@ -233,7 +233,7 @@ class _StudySessionScreenState extends ConsumerState void _flipCard() { setState(() { - _showingAnswer = true; + _showingAnswer = !_showingAnswer; }); } @@ -670,20 +670,27 @@ class _StudySessionScreenState extends ConsumerState return KeyEventResult.handled; } } else { - if (event.logicalKey == LogicalKeyboardKey.digit1) { - _dismissAndRate(Rating.again); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.digit2) { - _dismissAndRate(Rating.hard); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.digit3) { - _dismissAndRate(Rating.good); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.digit4) { - _dismissAndRate(Rating.easy); + if (event.logicalKey == LogicalKeyboardKey.space) { + _flipCard(); return KeyEventResult.handled; } } + + // Rating shortcuts work from either face + if (event.logicalKey == LogicalKeyboardKey.digit1) { + _dismissAndRate(Rating.again); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.digit2) { + _dismissAndRate(Rating.hard); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.digit3) { + _dismissAndRate(Rating.good); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.digit4) { + _dismissAndRate(Rating.easy); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; } @@ -765,7 +772,7 @@ class _StudySessionScreenState extends ConsumerState if (!isDesktop) { flipCard = SwipeableCard( key: ValueKey('swipe_$_currentIndex'), - enabled: _showingAnswer, + enabled: true, onRate: _rateCard, onDismissProgress: (progress) { if (progress != _swipeProgress) { @@ -822,12 +829,11 @@ class _StudySessionScreenState extends ConsumerState Widget _buildRatingButtons() { // Fade from full → dimmed as dismiss animation plays. - final baseOpacity = _showingAnswer ? 1.0 : 0.3; - final opacity = baseOpacity - (_dismissOffset * (baseOpacity - 0.3)); + final opacity = 1.0 - _dismissOffset; return Opacity( opacity: opacity, child: IgnorePointer( - ignoring: !_showingAnswer || _dismissOffset > 0, + ignoring: _dismissOffset > 0, child: Row( children: [ _RatingButton( diff --git a/lib/features/study/presentation/widgets/flip_card.dart b/lib/features/study/presentation/widgets/flip_card.dart index b784380..b289fcf 100644 --- a/lib/features/study/presentation/widgets/flip_card.dart +++ b/lib/features/study/presentation/widgets/flip_card.dart @@ -68,12 +68,10 @@ class _FlipCardState extends State final useVerticalFlip = size.width > size.height; return GestureDetector( - onTap: widget.isFlipped - ? null - : () { - HapticFeedback.lightImpact(); - widget.onFlip(); - }, + onTap: () { + HapticFeedback.lightImpact(); + widget.onFlip(); + }, child: AnimatedBuilder( animation: _animation, builder: (context, child) { From 161d48ee339da858c3c5c51f07cf45545d381ab8 Mon Sep 17 00:00:00 2001 From: djanderson26 Date: Sat, 18 Apr 2026 19:07:50 -0400 Subject: [PATCH 2/3] docs: update AI log to include usage for issues #128 and #133 --- docs/AI-Usage-Darius.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/AI-Usage-Darius.md b/docs/AI-Usage-Darius.md index 988d77a..ddc1c04 100644 --- a/docs/AI-Usage-Darius.md +++ b/docs/AI-Usage-Darius.md @@ -143,3 +143,21 @@ Input Summary: Team suggestions included: (1) separate "Sort By (Deck)" dropdown Output Summary: Implemented 6 major enhancements: deck dropdown with nested filtering, responsive layouts (mobile/desktop 600px breakpoint), filter summary display, Sort Order as button chips, SingleChildScrollView layout fix, and clearer `Icons.view_list` navigation icon. Modifications: Added `selectedDeckId` field and `CardSortBy.deck` enum, created `getByDeckIdWithNested()` recursive query, converted filter panel to ConsumerWidget, added responsive breakpoints and filter summary display, replaced Sort Order toggle with FilterChip buttons, fixed overflow with SingleChildScrollView, updated icon and added "Sort/Filter" label. Files Referenced: card_browser_filters.dart (added selectedDeckId, CardSortBy.deck), card_repository.dart (added getByDeckIdWithNested), card_browser_provider.dart (updated selectedDeckId references, added deck sort case), card_browser_screen.dart (added filter summary display, Sort/Filter label, SingleChildScrollView layout, filter summary builder), card_browser_filter_panel.dart (ConsumerWidget conversion, deck dropdown, responsive layouts, left-alignment chips, Order buttons), deck_list_screen.dart (icon/tooltip change to Icons.view_list) + +Date: 2026-04-18 +User: Darius Anderson +Purpose: Implement review data rework — cap reviews at 10K per user and record session summaries (#133) +Approach: Explored existing repository patterns and session screen structure, then requested implementation for 10K review pruning and session summary integration. +Input Summary: The review_session_summary table was already implemented (v5 migration). Needed client-side 10K review cap enforcement by pruning oldest reviews after each study session, plus integration with StudySessionScreen to record summaries on completion. +Output Summary: Received implementation plan for pruneOldReviews(userId) method for ReviewRepository that deletes oldest reviews exceeding 10K threshold, _finalizeSession() method for StudySessionScreen that creates/saves session summaries and triggers pruning, and comprehensive unit tests covering normal/edge cases. +Modifications: Added pruneOldReviews() to ReviewRepository with transaction-safe deletion logic, added card state tracking (_newCount, _learningCount, _reviewCount) to StudySessionScreen, integrated _finalizeSession() on session completion (both normal exit and early termination), created 6 test cases for pruning logic (all passing). +Files Referenced: review_repository.dart, study_session_screen.dart, review_session_summary_repository.dart, review_repository_test.dart + +Date: 2026-04-18 +User: Darius Anderson +Purpose: Allow bidirectional card flipping and rating from either face (#128) +Approach: Described the issue requirements and asked for implementation of tap-to-toggle, always-enabled swipe-to-rate, and keyboard shortcuts that work from both faces. +Input Summary: Provided issue description showing current behavior (card locked to back after flip) and desired behavior (freely flippable and ratable from either face). +Output Summary: Received implementation plan for changes to FlipCard (remove tap prevention), _flipCard() (toggle instead of set true), SwipeableCard (always enabled), keyboard handling (rate from either face), and rating buttons (always visible). +Modifications: Updated FlipCard to accept taps when flipped, changed _flipCard() to toggle state, enabled SwipeableCard always on mobile, allowed rating shortcuts from either face, made buttons visible on desktop from both faces. +Files Referenced: flip_card.dart, study_session_screen.dart \ No newline at end of file From 59fc6260321b4c1ea8cd7dfa646e2f4c678b23da Mon Sep 17 00:00:00 2001 From: djanderson26 Date: Wed, 22 Apr 2026 13:45:32 -0400 Subject: [PATCH 3/3] feat(study): require first flip before rating, allow rating from either face (#128) - Add _hasSeenAnswer state variable to track if answer has been revealed - Guard all rating inputs (keyboard, swipe, buttons) with _hasSeenAnswer flag - Reset flag on next card and when undoing - Enables rating from both front and back faces, but only after initial flip Fixes behavior where rating was possible before revealing the answer. --- docs/AI-Usage-Darius.md | 11 +++- .../screens/study_session_screen.dart | 53 ++++++++++--------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/docs/AI-Usage-Darius.md b/docs/AI-Usage-Darius.md index ddc1c04..0c8c664 100644 --- a/docs/AI-Usage-Darius.md +++ b/docs/AI-Usage-Darius.md @@ -160,4 +160,13 @@ Approach: Described the issue requirements and asked for implementation of tap-t Input Summary: Provided issue description showing current behavior (card locked to back after flip) and desired behavior (freely flippable and ratable from either face). Output Summary: Received implementation plan for changes to FlipCard (remove tap prevention), _flipCard() (toggle instead of set true), SwipeableCard (always enabled), keyboard handling (rate from either face), and rating buttons (always visible). Modifications: Updated FlipCard to accept taps when flipped, changed _flipCard() to toggle state, enabled SwipeableCard always on mobile, allowed rating shortcuts from either face, made buttons visible on desktop from both faces. -Files Referenced: flip_card.dart, study_session_screen.dart \ No newline at end of file +Files Referenced: lib/features/study/presentation/widgets/flip_card.dart, lib/features/study/presentation/screens/study_session_screen.dart + +Date: 2026-04-22 +User: Darius Anderson +Purpose: Refine bidirectional rating to require first flip before rating (#128) +Approach: Tested initial implementation and identified that rating was allowed before revealing the answer. Requested refinement to allow rating only after answer has been seen at least once, but from either face thereafter. +Input Summary: Provided test feedback showing rating worked before flip, which violated desired behavior of requiring first flip to reveal answer before rating. +Output Summary: Received refined solution: add _hasSeenAnswer boolean flag (distinct from _showingAnswer) that tracks if answer has been revealed at least once, use this flag to guard all rating inputs and swipe-to-rate, reset flag on next card. +Modifications: Added _hasSeenAnswer state variable initialized to false, set to true when _flipCard() reveals answer, reset to false when advancing to next card and when undoing, updated SwipeableCard.enabled, keyboard rating shortcuts, and rating buttons opacity/IgnorePointer conditions to use _hasSeenAnswer instead of _showingAnswer. +Files Referenced: lib/features/study/presentation/screens/study_session_screen.dart \ No newline at end of file diff --git a/lib/features/study/presentation/screens/study_session_screen.dart b/lib/features/study/presentation/screens/study_session_screen.dart index ae8a44f..d8332f0 100644 --- a/lib/features/study/presentation/screens/study_session_screen.dart +++ b/lib/features/study/presentation/screens/study_session_screen.dart @@ -132,6 +132,7 @@ class _StudySessionScreenState extends ConsumerState int _currentIndex = 0; bool _showingAnswer = false; + bool _hasSeenAnswer = false; // Tracks if answer has been revealed at least once final Map _ratingCounts = { Rating.again: 0, Rating.hard: 0, @@ -234,6 +235,9 @@ class _StudySessionScreenState extends ConsumerState void _flipCard() { setState(() { _showingAnswer = !_showingAnswer; + if (_showingAnswer) { + _hasSeenAnswer = true; + } }); } @@ -278,6 +282,7 @@ class _StudySessionScreenState extends ConsumerState } if (_reviewLog.isNotEmpty) _reviewLog.removeLast(); _showingAnswer = false; + _hasSeenAnswer = false; _dismissOffset = 0; _swipeProgress = 0; }); @@ -330,6 +335,7 @@ class _StudySessionScreenState extends ConsumerState ); _currentIndex++; _showingAnswer = false; + _hasSeenAnswer = false; }); } catch (e) { if (mounted) { @@ -664,33 +670,29 @@ class _StudySessionScreenState extends ConsumerState KeyEventResult _handleKeyPress(KeyEvent event) { if (event is! KeyDownEvent) return KeyEventResult.ignored; - if (!_showingAnswer) { - if (event.logicalKey == LogicalKeyboardKey.space) { - _flipCard(); + // Space always toggles flip + if (event.logicalKey == LogicalKeyboardKey.space) { + _flipCard(); + return KeyEventResult.handled; + } + + // Rating shortcuts work after answer has been revealed at least once + if (_hasSeenAnswer) { + if (event.logicalKey == LogicalKeyboardKey.digit1) { + _dismissAndRate(Rating.again); return KeyEventResult.handled; - } - } else { - if (event.logicalKey == LogicalKeyboardKey.space) { - _flipCard(); + } else if (event.logicalKey == LogicalKeyboardKey.digit2) { + _dismissAndRate(Rating.hard); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.digit3) { + _dismissAndRate(Rating.good); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.digit4) { + _dismissAndRate(Rating.easy); return KeyEventResult.handled; } } - // Rating shortcuts work from either face - if (event.logicalKey == LogicalKeyboardKey.digit1) { - _dismissAndRate(Rating.again); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.digit2) { - _dismissAndRate(Rating.hard); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.digit3) { - _dismissAndRate(Rating.good); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.digit4) { - _dismissAndRate(Rating.easy); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; } @@ -772,7 +774,7 @@ class _StudySessionScreenState extends ConsumerState if (!isDesktop) { flipCard = SwipeableCard( key: ValueKey('swipe_$_currentIndex'), - enabled: true, + enabled: _hasSeenAnswer, onRate: _rateCard, onDismissProgress: (progress) { if (progress != _swipeProgress) { @@ -829,11 +831,12 @@ class _StudySessionScreenState extends ConsumerState Widget _buildRatingButtons() { // Fade from full → dimmed as dismiss animation plays. - final opacity = 1.0 - _dismissOffset; + final baseOpacity = _hasSeenAnswer ? 1.0 : 0.3; + final opacity = baseOpacity - (_dismissOffset * (baseOpacity - 0.3)); return Opacity( opacity: opacity, child: IgnorePointer( - ignoring: _dismissOffset > 0, + ignoring: !_hasSeenAnswer || _dismissOffset > 0, child: Row( children: [ _RatingButton(