master
gitea 2 months ago
parent 9f06568eab
commit 68af4bf379

@ -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
),
];

@ -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());

@ -477,6 +477,8 @@ class _AttemptScreenState extends State<AttemptScreen> {
// 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(

@ -193,7 +193,7 @@ class _DeckListScreenState extends State<DeckListScreen> {
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<DeckListScreen> {
? 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

@ -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<DeckOverviewScreen> {
}
}
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<DeckOverviewScreen> {
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),
],
],
),

@ -14,17 +14,25 @@ class DeckStorage {
static const String _deckIdsKey = 'deck_ids';
bool _initialized = false;
SharedPreferences? _prefs;
final Future<void> _initFuture;
Future<void>? _initFuture;
DeckStorage._internal() : _initFuture = SharedPreferences.getInstance().then((prefs) {
_instance._prefs = prefs;
_instance._initialized = true;
});
DeckStorage._internal();
/// Lazy initialization of SharedPreferences
Future<void> _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<void> 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<void> _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<dynamic>? ?? [];
final attemptHistory = historyJson.map((entryJson) {
final entryMap = entryJson as Map<String, dynamic>;
// 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();

@ -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<AttemptHistoryEntry> 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 = <Offset>[];
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);

@ -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"

Loading…
Cancel
Save

Powered by TurnKey Linux.