no chart
This commit is contained in:
+21
-20
@@ -156,42 +156,43 @@ class DefaultDeck {
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
// Create mock attempt history (4 previous attempts)
|
// Create mock attempt history (4 previous attempts)
|
||||||
|
// Unknown percentage decreases over time as more questions become known
|
||||||
final attemptHistory = [
|
final attemptHistory = [
|
||||||
// First attempt - 6/10 correct (60%)
|
// First attempt - 6/10 correct (60%), ~85% unknown (17/20 questions)
|
||||||
AttemptHistoryEntry(
|
AttemptHistoryEntry(
|
||||||
timestamp: now.subtract(const Duration(days: 7)).millisecondsSinceEpoch,
|
timestamp: now.subtract(const Duration(days: 7)).millisecondsSinceEpoch,
|
||||||
totalQuestions: 10,
|
totalQuestions: 10,
|
||||||
correctAnswers: 6,
|
correctCount: 6,
|
||||||
incorrectAnswers: 4,
|
percentageCorrect: 60.0,
|
||||||
skippedAnswers: 0,
|
timeSpent: 245000, // milliseconds
|
||||||
timeSpentSeconds: 245,
|
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(
|
AttemptHistoryEntry(
|
||||||
timestamp: now.subtract(const Duration(days: 5)).millisecondsSinceEpoch,
|
timestamp: now.subtract(const Duration(days: 5)).millisecondsSinceEpoch,
|
||||||
totalQuestions: 10,
|
totalQuestions: 10,
|
||||||
correctAnswers: 7,
|
correctCount: 7,
|
||||||
incorrectAnswers: 3,
|
percentageCorrect: 70.0,
|
||||||
skippedAnswers: 0,
|
timeSpent: 198000, // milliseconds
|
||||||
timeSpentSeconds: 198,
|
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(
|
AttemptHistoryEntry(
|
||||||
timestamp: now.subtract(const Duration(days: 3)).millisecondsSinceEpoch,
|
timestamp: now.subtract(const Duration(days: 3)).millisecondsSinceEpoch,
|
||||||
totalQuestions: 10,
|
totalQuestions: 10,
|
||||||
correctAnswers: 8,
|
correctCount: 8,
|
||||||
incorrectAnswers: 2,
|
percentageCorrect: 80.0,
|
||||||
skippedAnswers: 0,
|
timeSpent: 187000, // milliseconds
|
||||||
timeSpentSeconds: 187,
|
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(
|
AttemptHistoryEntry(
|
||||||
timestamp: now.subtract(const Duration(days: 1)).millisecondsSinceEpoch,
|
timestamp: now.subtract(const Duration(days: 1)).millisecondsSinceEpoch,
|
||||||
totalQuestions: 10,
|
totalQuestions: 10,
|
||||||
correctAnswers: 9,
|
correctCount: 9,
|
||||||
incorrectAnswers: 1,
|
percentageCorrect: 90.0,
|
||||||
skippedAnswers: 0,
|
timeSpent: 165000, // milliseconds
|
||||||
timeSpentSeconds: 165,
|
unknownPercentage: 45.0, // 9 out of 20 questions unknown
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -3,7 +3,10 @@ import 'routes.dart';
|
|||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import 'services/deck_storage.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
|
// Initialize storage early
|
||||||
DeckStorage().initialize();
|
DeckStorage().initialize();
|
||||||
runApp(const DeckyApp());
|
runApp(const DeckyApp());
|
||||||
|
|||||||
@@ -477,6 +477,8 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
|||||||
// Add attempt to history
|
// Add attempt to history
|
||||||
final historyEntry = AttemptHistoryEntry.fromAttemptResult(
|
final historyEntry = AttemptHistoryEntry.fromAttemptResult(
|
||||||
result: result.result,
|
result: result.result,
|
||||||
|
totalQuestionsInDeck: result.updatedDeck.numberOfQuestions,
|
||||||
|
knownCount: result.updatedDeck.knownCount,
|
||||||
timestamp: endTime,
|
timestamp: endTime,
|
||||||
);
|
);
|
||||||
final updatedDeckWithHistory = result.updatedDeck.copyWith(
|
final updatedDeckWithHistory = result.updatedDeck.copyWith(
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
|||||||
void _useDefaultDeck() async {
|
void _useDefaultDeck() async {
|
||||||
// Check if default deck already exists
|
// Check if default deck already exists
|
||||||
final baseDeck = DefaultDeck.deck;
|
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)
|
// Show dialog to ask if user wants mock data (only if deck doesn't exist)
|
||||||
bool? includeMockData = false;
|
bool? includeMockData = false;
|
||||||
@@ -214,36 +214,47 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
|||||||
? DefaultDeck.deckWithMockData
|
? DefaultDeck.deckWithMockData
|
||||||
: DefaultDeck.deck;
|
: DefaultDeck.deck;
|
||||||
|
|
||||||
if (deckExists) {
|
try {
|
||||||
// If it exists, create a copy with a new ID
|
if (deckExists) {
|
||||||
final clonedDeck = defaultDeck.copyWith(
|
// If it exists, create a copy with a new ID
|
||||||
id: '${defaultDeck.id}_${DateTime.now().millisecondsSinceEpoch}',
|
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
await _deckStorage.saveDeck(clonedDeck);
|
||||||
} else {
|
|
||||||
// Save default deck to storage
|
|
||||||
_deckStorage.saveDeckSync(defaultDeck);
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadDecks();
|
||||||
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Default deck added successfully'),
|
content: Text('Error adding deck: $e'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadDecks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:practice_engine/practice_engine.dart';
|
import 'package:practice_engine/practice_engine.dart';
|
||||||
import '../routes.dart';
|
import '../routes.dart';
|
||||||
import '../services/deck_storage.dart';
|
import '../services/deck_storage.dart';
|
||||||
import '../widgets/attempts_chart.dart';
|
|
||||||
|
|
||||||
class DeckOverviewScreen extends StatefulWidget {
|
class DeckOverviewScreen extends StatefulWidget {
|
||||||
const DeckOverviewScreen({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -462,15 +560,12 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Progress Chart',
|
'Learning Progress',
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 12),
|
||||||
// Progress chart
|
// Current progress
|
||||||
AttemptsChart(
|
_buildProgressStats(context),
|
||||||
attempts: _deck!.attemptHistory,
|
|
||||||
maxDisplayItems: 15,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,17 +14,25 @@ class DeckStorage {
|
|||||||
static const String _deckIdsKey = 'deck_ids';
|
static const String _deckIdsKey = 'deck_ids';
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
SharedPreferences? _prefs;
|
SharedPreferences? _prefs;
|
||||||
final Future<void> _initFuture;
|
Future<void>? _initFuture;
|
||||||
|
|
||||||
DeckStorage._internal() : _initFuture = SharedPreferences.getInstance().then((prefs) {
|
DeckStorage._internal();
|
||||||
_instance._prefs = prefs;
|
|
||||||
_instance._initialized = true;
|
/// 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
|
/// Initialize storage with default deck if empty
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
await _initFuture;
|
await _initSharedPreferences();
|
||||||
|
|
||||||
// Load existing decks
|
// Load existing decks
|
||||||
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
|
final deckIds = _prefs!.getStringList(_deckIdsKey) ?? [];
|
||||||
@@ -39,7 +47,7 @@ class DeckStorage {
|
|||||||
/// Ensure storage is initialized
|
/// Ensure storage is initialized
|
||||||
Future<void> _ensureInitialized() async {
|
Future<void> _ensureInitialized() async {
|
||||||
if (!_initialized) {
|
if (!_initialized) {
|
||||||
await _initFuture;
|
await _initSharedPreferences();
|
||||||
await initialize();
|
await initialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,10 +83,10 @@ class DeckStorage {
|
|||||||
'attemptHistory': deck.attemptHistory.map((entry) => {
|
'attemptHistory': deck.attemptHistory.map((entry) => {
|
||||||
'timestamp': entry.timestamp,
|
'timestamp': entry.timestamp,
|
||||||
'totalQuestions': entry.totalQuestions,
|
'totalQuestions': entry.totalQuestions,
|
||||||
'correctAnswers': entry.correctAnswers,
|
'correctCount': entry.correctCount,
|
||||||
'incorrectAnswers': entry.incorrectAnswers,
|
'percentageCorrect': entry.percentageCorrect,
|
||||||
'skippedAnswers': entry.skippedAnswers,
|
'timeSpent': entry.timeSpent,
|
||||||
'timeSpentSeconds': entry.timeSpentSeconds,
|
'unknownPercentage': entry.unknownPercentage,
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'incompleteAttempt': deck.incompleteAttempt != null ? {
|
'incompleteAttempt': deck.incompleteAttempt != null ? {
|
||||||
'attemptId': deck.incompleteAttempt!.attemptId,
|
'attemptId': deck.incompleteAttempt!.attemptId,
|
||||||
@@ -145,13 +153,23 @@ class DeckStorage {
|
|||||||
final historyJson = json['attemptHistory'] as List<dynamic>? ?? [];
|
final historyJson = json['attemptHistory'] as List<dynamic>? ?? [];
|
||||||
final attemptHistory = historyJson.map((entryJson) {
|
final attemptHistory = historyJson.map((entryJson) {
|
||||||
final entryMap = entryJson as Map<String, dynamic>;
|
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(
|
return AttemptHistoryEntry(
|
||||||
timestamp: entryMap['timestamp'] as int? ?? 0,
|
timestamp: entryMap['timestamp'] as int? ?? 0,
|
||||||
totalQuestions: entryMap['totalQuestions'] as int? ?? 0,
|
totalQuestions: totalQuestions,
|
||||||
correctAnswers: entryMap['correctAnswers'] as int? ?? 0,
|
correctCount: correctCount,
|
||||||
incorrectAnswers: entryMap['incorrectAnswers'] as int? ?? 0,
|
percentageCorrect: percentageCorrect,
|
||||||
skippedAnswers: entryMap['skippedAnswers'] as int? ?? 0,
|
timeSpent: timeSpent,
|
||||||
timeSpentSeconds: entryMap['timeSpentSeconds'] as int?,
|
unknownPercentage: unknownPercentage,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:practice_engine/practice_engine.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 {
|
class AttemptsChart extends StatelessWidget {
|
||||||
final List<AttemptHistoryEntry> attempts;
|
final List<AttemptHistoryEntry> attempts;
|
||||||
final int maxDisplayItems;
|
final int maxDisplayItems;
|
||||||
@@ -18,14 +20,14 @@ class AttemptsChart extends StatelessWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get recent attempts (most recent first, then reverse for display)
|
// Get recent attempts (take most recent, then reverse so oldest is first for chronological display)
|
||||||
final displayAttempts = attempts.reversed.take(maxDisplayItems).toList();
|
final displayAttempts = attempts.reversed.take(maxDisplayItems).toList().reversed.toList();
|
||||||
if (displayAttempts.isEmpty) {
|
if (displayAttempts.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find min and max values for scaling
|
// Find min and max values for scaling (using unknown percentage)
|
||||||
final percentages = displayAttempts.map((e) => e.percentageCorrect).toList();
|
final percentages = displayAttempts.map((e) => e.unknownPercentage).toList();
|
||||||
final minValue = percentages.reduce((a, b) => a < b ? a : b);
|
final minValue = percentages.reduce((a, b) => a < b ? a : b);
|
||||||
final maxValue = 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
|
final range = (maxValue - minValue).clamp(10.0, 100.0); // Ensure minimum range
|
||||||
@@ -148,7 +150,7 @@ class _AttemptsChartPainter extends CustomPainter {
|
|||||||
final points = <Offset>[];
|
final points = <Offset>[];
|
||||||
|
|
||||||
for (int i = 0; i < attempts.length; i++) {
|
for (int i = 0; i < attempts.length; i++) {
|
||||||
final percentage = attempts[i].percentageCorrect;
|
final percentage = attempts[i].unknownPercentage;
|
||||||
final normalizedValue = chartRange > 0
|
final normalizedValue = chartRange > 0
|
||||||
? ((percentage - chartMin) / chartRange).clamp(0.0, 1.0)
|
? ((percentage - chartMin) / chartRange).clamp(0.0, 1.0)
|
||||||
: 0.5; // If all values are the same, center vertically
|
: 0.5; // If all values are the same, center vertically
|
||||||
@@ -201,7 +203,7 @@ class _AttemptsChartPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw average line (dashed)
|
// 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 avgNormalized = ((avgPercentage - chartMin) / chartRange).clamp(0.0, 1.0);
|
||||||
final avgY = padding + chartHeight - (avgNormalized * chartHeight);
|
final avgY = padding + chartHeight - (avgNormalized * chartHeight);
|
||||||
|
|
||||||
|
|||||||
+135
-2
@@ -57,6 +57,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -75,6 +91,11 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -139,6 +160,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
practice_engine:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -146,6 +207,62 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.0"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -215,6 +332,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
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:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user