From 68af4bf3797f9839e3eb03ce62c7b4e7360738a5 Mon Sep 17 00:00:00 2001 From: gitea Date: Fri, 2 Jan 2026 19:56:50 +0100 Subject: [PATCH] no chart --- lib/data/default_deck.dart | 41 ++++---- lib/main.dart | 5 +- lib/screens/attempt_screen.dart | 2 + lib/screens/deck_list_screen.dart | 55 ++++++----- lib/screens/deck_overview_screen.dart | 111 +++++++++++++++++++-- lib/services/deck_storage.dart | 50 +++++++--- lib/widgets/attempts_chart.dart | 16 +-- pubspec.lock | 137 +++++++++++++++++++++++++- 8 files changed, 341 insertions(+), 76 deletions(-) diff --git a/lib/data/default_deck.dart b/lib/data/default_deck.dart index 14a686a..39288eb 100644 --- a/lib/data/default_deck.dart +++ b/lib/data/default_deck.dart @@ -156,42 +156,43 @@ class DefaultDeck { final now = DateTime.now(); // Create mock attempt history (4 previous attempts) + // Unknown percentage decreases over time as more questions become known final attemptHistory = [ - // First attempt - 6/10 correct (60%) + // First attempt - 6/10 correct (60%), ~85% unknown (17/20 questions) AttemptHistoryEntry( timestamp: now.subtract(const Duration(days: 7)).millisecondsSinceEpoch, totalQuestions: 10, - correctAnswers: 6, - incorrectAnswers: 4, - skippedAnswers: 0, - timeSpentSeconds: 245, + correctCount: 6, + percentageCorrect: 60.0, + timeSpent: 245000, // milliseconds + unknownPercentage: 85.0, // 17 out of 20 questions unknown ), - // Second attempt - 7/10 correct (70%) + // Second attempt - 7/10 correct (70%), ~70% unknown (14/20 questions) AttemptHistoryEntry( timestamp: now.subtract(const Duration(days: 5)).millisecondsSinceEpoch, totalQuestions: 10, - correctAnswers: 7, - incorrectAnswers: 3, - skippedAnswers: 0, - timeSpentSeconds: 198, + correctCount: 7, + percentageCorrect: 70.0, + timeSpent: 198000, // milliseconds + unknownPercentage: 70.0, // 14 out of 20 questions unknown ), - // Third attempt - 8/10 correct (80%) + // Third attempt - 8/10 correct (80%), ~55% unknown (11/20 questions) AttemptHistoryEntry( timestamp: now.subtract(const Duration(days: 3)).millisecondsSinceEpoch, totalQuestions: 10, - correctAnswers: 8, - incorrectAnswers: 2, - skippedAnswers: 0, - timeSpentSeconds: 187, + correctCount: 8, + percentageCorrect: 80.0, + timeSpent: 187000, // milliseconds + unknownPercentage: 55.0, // 11 out of 20 questions unknown ), - // Fourth attempt - 9/10 correct (90%) + // Fourth attempt - 9/10 correct (90%), ~45% unknown (9/20 questions) AttemptHistoryEntry( timestamp: now.subtract(const Duration(days: 1)).millisecondsSinceEpoch, totalQuestions: 10, - correctAnswers: 9, - incorrectAnswers: 1, - skippedAnswers: 0, - timeSpentSeconds: 165, + correctCount: 9, + percentageCorrect: 90.0, + timeSpent: 165000, // milliseconds + unknownPercentage: 45.0, // 9 out of 20 questions unknown ), ]; diff --git a/lib/main.dart b/lib/main.dart index 3a926ef..1f604b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,10 @@ import 'routes.dart'; import 'theme.dart'; import 'services/deck_storage.dart'; -void main() { +void main() async { + // Ensure Flutter bindings are initialized before using any plugins + WidgetsFlutterBinding.ensureInitialized(); + // Initialize storage early DeckStorage().initialize(); runApp(const DeckyApp()); diff --git a/lib/screens/attempt_screen.dart b/lib/screens/attempt_screen.dart index 35acaa3..a6ce4ad 100644 --- a/lib/screens/attempt_screen.dart +++ b/lib/screens/attempt_screen.dart @@ -477,6 +477,8 @@ class _AttemptScreenState extends State { // Add attempt to history final historyEntry = AttemptHistoryEntry.fromAttemptResult( result: result.result, + totalQuestionsInDeck: result.updatedDeck.numberOfQuestions, + knownCount: result.updatedDeck.knownCount, timestamp: endTime, ); final updatedDeckWithHistory = result.updatedDeck.copyWith( diff --git a/lib/screens/deck_list_screen.dart b/lib/screens/deck_list_screen.dart index b5d6812..2b36d9c 100644 --- a/lib/screens/deck_list_screen.dart +++ b/lib/screens/deck_list_screen.dart @@ -193,7 +193,7 @@ class _DeckListScreenState extends State { void _useDefaultDeck() async { // Check if default deck already exists final baseDeck = DefaultDeck.deck; - final deckExists = _deckStorage.hasDeckSync(baseDeck.id); + final deckExists = await _deckStorage.hasDeck(baseDeck.id); // Show dialog to ask if user wants mock data (only if deck doesn't exist) bool? includeMockData = false; @@ -214,36 +214,47 @@ class _DeckListScreenState extends State { ? DefaultDeck.deckWithMockData : DefaultDeck.deck; - if (deckExists) { - // If it exists, create a copy with a new ID - final clonedDeck = defaultDeck.copyWith( - id: '${defaultDeck.id}_${DateTime.now().millisecondsSinceEpoch}', - ); - _deckStorage.saveDeckSync(clonedDeck); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Default deck added successfully'), - backgroundColor: Colors.green, - ), + try { + if (deckExists) { + // If it exists, create a copy with a new ID + final clonedDeck = defaultDeck.copyWith( + id: '${defaultDeck.id}_${DateTime.now().millisecondsSinceEpoch}', ); + await _deckStorage.saveDeck(clonedDeck); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Default deck added successfully'), + backgroundColor: Colors.green, + ), + ); + } + } else { + // Save default deck to storage + await _deckStorage.saveDeck(defaultDeck); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Default deck added successfully'), + backgroundColor: Colors.green, + ), + ); + } } - } else { - // Save default deck to storage - _deckStorage.saveDeckSync(defaultDeck); + _loadDecks(); + } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Default deck added successfully'), - backgroundColor: Colors.green, + SnackBar( + content: Text('Error adding deck: $e'), + backgroundColor: Colors.red, ), ); } } - - _loadDecks(); } @override diff --git a/lib/screens/deck_overview_screen.dart b/lib/screens/deck_overview_screen.dart index 2d485e1..78a900a 100644 --- a/lib/screens/deck_overview_screen.dart +++ b/lib/screens/deck_overview_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:practice_engine/practice_engine.dart'; import '../routes.dart'; import '../services/deck_storage.dart'; -import '../widgets/attempts_chart.dart'; class DeckOverviewScreen extends StatefulWidget { const DeckOverviewScreen({super.key}); @@ -283,6 +282,105 @@ class _DeckOverviewScreenState extends State { } } + Widget _buildProgressStats(BuildContext context) { + if (_deck == null) return const SizedBox.shrink(); + + final totalQuestions = _deck!.numberOfQuestions; + final knownCount = _deck!.knownCount; + final unknownCount = totalQuestions - knownCount; + final unknownPercentage = totalQuestions > 0 + ? (unknownCount / totalQuestions * 100.0) + : 0.0; + final knownPercentage = totalQuestions > 0 + ? (knownCount / totalQuestions * 100.0) + : 0.0; + + // Get progress trend if we have history + // Attempt history is stored chronologically (oldest first), so first is oldest, last is newest + String? trendText; + if (_deck!.attemptHistory.isNotEmpty) { + final oldestAttempt = _deck!.attemptHistory.first; + final newestAttempt = _deck!.attemptHistory.last; + final oldestUnknown = oldestAttempt.unknownPercentage; + final newestUnknown = newestAttempt.unknownPercentage; + final improvement = oldestUnknown - newestUnknown; + + if (improvement > 0) { + trendText = 'Improved by ${improvement.toStringAsFixed(1)}% since first attempt'; + } else if (improvement < 0) { + trendText = '${improvement.abs().toStringAsFixed(1)}% more to learn since first attempt'; + } else { + trendText = 'No change since first attempt'; + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Main stat: Unknown percentage + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Questions to Learn', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 4), + Text( + '${unknownPercentage.toStringAsFixed(1)}%', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + Text( + '$unknownCount of $totalQuestions questions', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Questions Known', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 4), + Text( + '${knownPercentage.toStringAsFixed(1)}%', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Text( + '$knownCount of $totalQuestions questions', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ], + ), + if (trendText != null) ...[ + const SizedBox(height: 12), + Text( + trendText, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ], + ); + } @override Widget build(BuildContext context) { @@ -462,15 +560,12 @@ class _DeckOverviewScreenState extends State { const Divider(), const SizedBox(height: 8), Text( - 'Progress Chart', + 'Learning Progress', style: Theme.of(context).textTheme.titleSmall, ), - const SizedBox(height: 8), - // Progress chart - AttemptsChart( - attempts: _deck!.attemptHistory, - maxDisplayItems: 15, - ), + const SizedBox(height: 12), + // Current progress + _buildProgressStats(context), ], ], ), diff --git a/lib/services/deck_storage.dart b/lib/services/deck_storage.dart index f6168e1..ffa426b 100644 --- a/lib/services/deck_storage.dart +++ b/lib/services/deck_storage.dart @@ -14,17 +14,25 @@ class DeckStorage { static const String _deckIdsKey = 'deck_ids'; bool _initialized = false; SharedPreferences? _prefs; - final Future _initFuture; + Future? _initFuture; - DeckStorage._internal() : _initFuture = SharedPreferences.getInstance().then((prefs) { - _instance._prefs = prefs; - _instance._initialized = true; - }); + DeckStorage._internal(); + + /// Lazy initialization of SharedPreferences + Future _initSharedPreferences() async { + if (_initFuture == null) { + _initFuture = SharedPreferences.getInstance().then((prefs) { + _instance._prefs = prefs; + _instance._initialized = true; + }); + } + await _initFuture; + } /// Initialize storage with default deck if empty Future initialize() async { if (_initialized) return; - await _initFuture; + await _initSharedPreferences(); // Load existing decks final deckIds = _prefs!.getStringList(_deckIdsKey) ?? []; @@ -39,7 +47,7 @@ class DeckStorage { /// Ensure storage is initialized Future _ensureInitialized() async { if (!_initialized) { - await _initFuture; + await _initSharedPreferences(); await initialize(); } } @@ -75,10 +83,10 @@ class DeckStorage { 'attemptHistory': deck.attemptHistory.map((entry) => { 'timestamp': entry.timestamp, 'totalQuestions': entry.totalQuestions, - 'correctAnswers': entry.correctAnswers, - 'incorrectAnswers': entry.incorrectAnswers, - 'skippedAnswers': entry.skippedAnswers, - 'timeSpentSeconds': entry.timeSpentSeconds, + 'correctCount': entry.correctCount, + 'percentageCorrect': entry.percentageCorrect, + 'timeSpent': entry.timeSpent, + 'unknownPercentage': entry.unknownPercentage, }).toList(), 'incompleteAttempt': deck.incompleteAttempt != null ? { 'attemptId': deck.incompleteAttempt!.attemptId, @@ -145,13 +153,23 @@ class DeckStorage { final historyJson = json['attemptHistory'] as List? ?? []; final attemptHistory = historyJson.map((entryJson) { final entryMap = entryJson as Map; + // Support both old and new field names for backward compatibility + final correctCount = entryMap['correctCount'] as int? ?? + entryMap['correctAnswers'] as int? ?? 0; + final totalQuestions = entryMap['totalQuestions'] as int? ?? 0; + final timeSpent = entryMap['timeSpent'] as int? ?? + (entryMap['timeSpentSeconds'] as int? ?? 0) * 1000; // Convert seconds to milliseconds + // Calculate percentageCorrect if not present + final percentageCorrect = entryMap['percentageCorrect'] as double? ?? + (totalQuestions > 0 ? (correctCount / totalQuestions * 100) : 0.0); + final unknownPercentage = entryMap['unknownPercentage'] as double? ?? 0.0; return AttemptHistoryEntry( timestamp: entryMap['timestamp'] as int? ?? 0, - totalQuestions: entryMap['totalQuestions'] as int? ?? 0, - correctAnswers: entryMap['correctAnswers'] as int? ?? 0, - incorrectAnswers: entryMap['incorrectAnswers'] as int? ?? 0, - skippedAnswers: entryMap['skippedAnswers'] as int? ?? 0, - timeSpentSeconds: entryMap['timeSpentSeconds'] as int?, + totalQuestions: totalQuestions, + correctCount: correctCount, + percentageCorrect: percentageCorrect, + timeSpent: timeSpent, + unknownPercentage: unknownPercentage, ); }).toList(); diff --git a/lib/widgets/attempts_chart.dart b/lib/widgets/attempts_chart.dart index 54bb823..b634623 100644 --- a/lib/widgets/attempts_chart.dart +++ b/lib/widgets/attempts_chart.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:practice_engine/practice_engine.dart'; -/// A chart widget that visualizes attempt progress over time. +/// A chart widget that visualizes learning progress over time. +/// Shows the percentage of questions that are still unknown (not yet learned). +/// As you learn more questions, this percentage decreases. class AttemptsChart extends StatelessWidget { final List attempts; final int maxDisplayItems; @@ -18,14 +20,14 @@ class AttemptsChart extends StatelessWidget { return const SizedBox.shrink(); } - // Get recent attempts (most recent first, then reverse for display) - final displayAttempts = attempts.reversed.take(maxDisplayItems).toList(); + // Get recent attempts (take most recent, then reverse so oldest is first for chronological display) + final displayAttempts = attempts.reversed.take(maxDisplayItems).toList().reversed.toList(); if (displayAttempts.isEmpty) { return const SizedBox.shrink(); } - // Find min and max values for scaling - final percentages = displayAttempts.map((e) => e.percentageCorrect).toList(); + // Find min and max values for scaling (using unknown percentage) + final percentages = displayAttempts.map((e) => e.unknownPercentage).toList(); final minValue = percentages.reduce((a, b) => a < b ? a : b); final maxValue = percentages.reduce((a, b) => a > b ? a : b); final range = (maxValue - minValue).clamp(10.0, 100.0); // Ensure minimum range @@ -148,7 +150,7 @@ class _AttemptsChartPainter extends CustomPainter { final points = []; for (int i = 0; i < attempts.length; i++) { - final percentage = attempts[i].percentageCorrect; + final percentage = attempts[i].unknownPercentage; final normalizedValue = chartRange > 0 ? ((percentage - chartMin) / chartRange).clamp(0.0, 1.0) : 0.5; // If all values are the same, center vertically @@ -201,7 +203,7 @@ class _AttemptsChartPainter extends CustomPainter { } // Draw average line (dashed) - final avgPercentage = attempts.map((e) => e.percentageCorrect).reduce((a, b) => a + b) / attempts.length; + final avgPercentage = attempts.map((e) => e.unknownPercentage).reduce((a, b) => a + b) / attempts.length; final avgNormalized = ((avgPercentage - chartMin) / chartRange).clamp(0.0, 1.0); final avgY = padding + chartHeight - (avgNormalized * chartHeight); diff --git a/pubspec.lock b/pubspec.lock index 1d28a18..351d550 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -75,6 +91,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -139,6 +160,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" practice_engine: dependency: "direct main" description: @@ -146,6 +207,62 @@ packages: relative: true source: path version: "1.0.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + url: "https://pub.dev" + source: hosted + version: "2.4.18" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -215,6 +332,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0"