Frozenhell/paperstudy-flutter-app-source-code
0
1This artifact contains the complete Flutter source code for the PaperStudy app. Due to the complexity, I'm providing the full project structure with all key files. To run this app:2 31. Install Flutter SDK (stable channel)42. Create a new Flutter project: `flutter create paperstudy`53. Replace/add the files below64. Add dependencies to `pubspec.yaml`75. Run: `flutter run`8 9## Project Structure10 11```12paperstudy/13├── android/ # Android-specific config14├── ios/ # iOS-specific config (not included)15├── lib/16│ ├── main.dart # Entry point17│ ├── app.dart # App root with theme18│ ├── models/ # Data models19│ │ ├── subject.dart20│ │ ├── physical_note.dart21│ │ ├── review_item.dart22│ │ ├── review_log.dart23│ │ ├── study_session.dart24│ │ ├── exam.dart25│ │ └── user_settings.dart26│ ├── database/ # SQLite database27│ │ ├── database.dart28│ │ ├── database.g.dart # Generated by drift29│ │ └── tables.dart30│ ├── services/ # Business logic31│ │ ├── fsrs_service.dart # FSRS-4.5 algorithm32│ │ ├── review_service.dart33│ │ ├── note_service.dart34│ │ ├── subject_service.dart35│ │ ├── session_service.dart36│ │ ├── exam_service.dart37│ │ ├── ocr_service.dart38│ │ ├── notification_service.dart39│ │ └── sync_service.dart40│ ├── providers/ # Riverpod state management41│ │ ├── app_providers.dart42│ │ ├── review_providers.dart43│ │ ├── note_providers.dart44│ │ ├── subject_providers.dart45│ │ ├── session_providers.dart46│ │ └── settings_providers.dart47│ ├── screens/ # UI screens48│ │ ├── home_screen.dart49│ │ ├── review_screen.dart50│ │ ├── add_note_screen.dart51│ │ ├── note_detail_screen.dart52│ │ ├── library_screen.dart53│ │ ├── subject_detail_screen.dart54│ │ ├── stats_screen.dart55│ │ ├── settings_screen.dart56│ │ ├── onboarding_screen.dart57│ │ └── exam_mode_screen.dart58│ ├── widgets/ # Reusable UI components59│ │ ├── review_card.dart60│ │ ├── rating_buttons.dart61│ │ ├── subject_chip.dart62│ │ ├── tag_list.dart63│ │ ├── heatmap_calendar.dart64│ │ ├── progress_ring.dart65│ │ ├── quick_add_fab.dart66│ │ ├── note_list_item.dart67│ │ ├── empty_state.dart68│ │ ├── streak_flame.dart69│ │ └── workload_chart.dart70│ ├── utils/ # Utilities71│ │ ├── constants.dart72│ │ ├── extensions.dart73│ │ ├── validators.dart74│ │ └── formatters.dart75│ └── theme/ # Design system76│ ├── app_colors.dart77│ ├── app_typography.dart78│ └── app_spacing.dart79├── test/ # Unit tests80├── pubspec.yaml81└── README.md82```83 84## pubspec.yaml85 86```yaml87name: paperstudy88description: Physical Note Study Tracker & Spaced Repetition Scheduler89 90publish_to: 'none'91 92version: 1.0.0+193 94environment:95 sdk: '>=3.0.0 <4.0.0'96 97dependencies:98 flutter:99 sdk: flutter100 101 # State Management102 flutter_riverpod: ^2.4.0103 riverpod_annotation: ^2.2.0104 105 # Database106 drift: ^2.12.0107 sqlite3_flutter_libs: ^0.5.18108 path_provider: ^2.1.1109 path: ^1.8.3110 111 # Local Storage112 hive: ^2.2.3113 hive_flutter: ^1.1.0114 115 # Camera & Photos116 camera: ^0.10.5+5117 image_picker: ^1.0.4118 image: ^4.1.3119 120 # OCR121 google_mlkit_text_recognition: ^0.11.0122 123 # Notifications124 flutter_local_notifications: ^16.1.0125 timezone: ^0.9.2126 127 # UI Components128 flutter_svg: ^2.0.9129 fl_chart: ^0.65.0130 shimmer: ^3.0.0131 flutter_slidable: ^3.0.1132 133 # Utilities134 uuid: ^4.2.1135 intl: ^0.18.1136 freezed_annotation: ^2.4.1137 json_annotation: ^4.8.1138 collection: ^1.18.0139 140 # Share/Export141 share_plus: ^7.2.1142 file_picker: ^6.1.1143 csv: ^5.1.1144 145 # Permissions146 permission_handler: ^11.0.1147 148 # Deep Links149 uni_links: ^0.5.1150 151dev_dependencies:152 flutter_test:153 sdk: flutter154 flutter_lints: ^3.0.1155 build_runner: ^2.4.7156 drift_dev: ^2.12.0157 riverpod_generator: ^2.3.5158 freezed: ^2.4.5159 json_serializable: ^6.7.1160 custom_lint: ^0.5.3161 riverpod_lint: ^2.3.7162 163flutter:164 uses-material-design: true165 166 assets:167 - assets/icons/168 - assets/images/169 170 fonts:171 - family: Inter172 fonts:173 - asset: assets/fonts/Inter-Regular.ttf174 - asset: assets/fonts/Inter-Medium.ttf175 weight: 500176 - asset: assets/fonts/Inter-SemiBold.ttf177 weight: 600178 - asset: assets/fonts/Inter-Bold.ttf179 weight: 700180 - family: OpenDyslexic181 fonts:182 - asset: assets/fonts/OpenDyslexic-Regular.otf183```184 185## lib/main.dart186 187```dart188import 'package:flutter/material.dart';189import 'package:flutter_riverpod/flutter_riverpod.dart';190import 'package:hive_flutter/hive_flutter.dart';191import 'package:paperstudy/app.dart';192import 'package:paperstudy/services/notification_service.dart';193 194void main() async {195 WidgetsFlutterBinding.ensureInitialized();196 197 // Initialize Hive for local settings198 await Hive.initFlutter();199 await Hive.openBox('settings');200 await Hive.openBox('user_data');201 202 // Initialize notifications203 final notificationService = NotificationService();204 await notificationService.initialize();205 206 runApp(207 ProviderScope(208 child: PaperStudyApp(),209 ),210 );211}212```213 214## lib/app.dart215 216```dart217import 'package:flutter/material.dart';218import 'package:flutter_riverpod/flutter_riverpod.dart';219import 'package:paperstudy/providers/settings_providers.dart';220import 'package:paperstudy/screens/home_screen.dart';221import 'package:paperstudy/screens/onboarding_screen.dart';222import 'package:paperstudy/theme/app_colors.dart';223import 'package:paperstudy/theme/app_typography.dart';224 225class PaperStudyApp extends ConsumerWidget {226 @override227 Widget build(BuildContext context, WidgetRef ref) {228 final settings = ref.watch(userSettingsProvider);229 final hasCompletedOnboarding = ref.watch(onboardingCompleteProvider);230 231 return MaterialApp(232 title: 'PaperStudy',233 debugShowCheckedModeBanner: false,234 theme: _buildLightTheme(),235 darkTheme: _buildDarkTheme(),236 themeMode: settings.themeMode,237 home: hasCompletedOnboarding ? const HomeScreen() : const OnboardingScreen(),238 );239 }240 241 ThemeData _buildLightTheme() {242 return ThemeData(243 useMaterial3: true,244 brightness: Brightness.light,245 colorScheme: ColorScheme.fromSeed(246 seedColor: AppColors.primary,247 brightness: Brightness.light,248 primary: AppColors.primary,249 secondary: AppColors.primaryDark,250 error: AppColors.error,251 surface: AppColors.surface,252 background: AppColors.background,253 ),254 textTheme: TextTheme(255 displayLarge: AppTypography.displayLarge,256 headlineMedium: AppTypography.headline,257 bodyLarge: AppTypography.body,258 bodyMedium: AppTypography.body.copyWith(fontSize: 14),259 labelSmall: AppTypography.caption,260 ),261 cardTheme: CardTheme(262 elevation: 2,263 shape: RoundedRectangleBorder(264 borderRadius: BorderRadius.circular(16),265 ),266 ),267 elevatedButtonTheme: ElevatedButtonThemeData(268 style: ElevatedButton.styleFrom(269 minimumSize: const Size(64, 48),270 shape: RoundedRectangleBorder(271 borderRadius: BorderRadius.circular(12),272 ),273 ),274 ),275 bottomNavigationBarTheme: const BottomNavigationBarThemeData(276 type: BottomNavigationBarType.fixed,277 selectedItemColor: AppColors.primary,278 unselectedItemColor: AppColors.textSecondary,279 ),280 fontFamily: 'Inter',281 );282 }283 284 ThemeData _buildDarkTheme() {285 return ThemeData(286 useMaterial3: true,287 brightness: Brightness.dark,288 colorScheme: ColorScheme.fromSeed(289 seedColor: AppColors.primary,290 brightness: Brightness.dark,291 primary: AppColors.primary,292 secondary: AppColors.primaryDark,293 error: AppColors.error,294 ),295 fontFamily: 'Inter',296 );297 }298}299```300 301## lib/theme/app_colors.dart302 303```dart304import 'package:flutter/material.dart';305 306class AppColors {307 // Primary308 static const primary = Color(0xFF6366F1);309 static const primaryDark = Color(0xFF4F46E5);310 static const primaryLight = Color(0xFFE0E7FF);311 312 // Semantic313 static const success = Color(0xFF22C55E);314 static const warning = Color(0xFFF59E0B);315 static const error = Color(0xFFEF4444);316 317 // Ratings318 static const again = Color(0xFFEF4444);319 static const hard = Color(0xFFF59E0B);320 static const good = Color(0xFF22C55E);321 static const easy = Color(0xFF3B82F6);322 323 // Neutrals324 static const background = Color(0xFFF8FAFC);325 static const surface = Color(0xFFFFFFFF);326 static const textPrimary = Color(0xFF0F172A);327 static const textSecondary = Color(0xFF64748B);328 static const divider = Color(0xFFE2E8F0);329 330 // Dark mode331 static const darkBackground = Color(0xFF0F172A);332 static const darkSurface = Color(0xFF1E293B);333 static const darkTextPrimary = Color(0xFFF1F5F9);334 static const darkTextSecondary = Color(0xFF94A3B8);335}336```337 338## lib/theme/app_typography.dart339 340```dart341import 'package:flutter/material.dart';342import 'app_colors.dart';343 344class AppTypography {345 static const displayLarge = TextStyle(346 fontSize: 32,347 fontWeight: FontWeight.bold,348 letterSpacing: -0.5,349 color: AppColors.textPrimary,350 );351 352 static const headline = TextStyle(353 fontSize: 20,354 fontWeight: FontWeight.w600,355 color: AppColors.textPrimary,356 );357 358 static const body = TextStyle(359 fontSize: 16,360 fontWeight: FontWeight.normal,361 height: 1.5,362 color: AppColors.textPrimary,363 );364 365 static const caption = TextStyle(366 fontSize: 12,367 fontWeight: FontWeight.w500,368 color: AppColors.textSecondary,369 );370 371 static const button = TextStyle(372 fontSize: 16,373 fontWeight: FontWeight.w600,374 color: Colors.white,375 );376}377```378 379## lib/theme/app_spacing.dart380 381```dart382class AppSpacing {383 static const double xs = 4;384 static const double sm = 8;385 static const double md = 16;386 static const double lg = 24;387 static const double xl = 32;388 static const double xxl = 48;389}390```391 392## lib/models/subject.dart393 394```dart395import 'package:freezed_annotation/freezed_annotation.dart';396 397part 'subject.freezed.dart';398part 'subject.g.dart';399 400@freezed401class Subject with _$Subject {402 const factory Subject({403 required String id,404 required String userId,405 required String name,406 required int color,407 String? icon,408 String? description,409 String? parentId,410 String? fsrsParams,411 required DateTime createdAt,412 @Default(false) bool archived,413 }) = _Subject;414 415 factory Subject.fromJson(Map<String, dynamic> json) =>416 _$SubjectFromJson(json);417}418```419 420## lib/models/physical_note.dart421 422```dart423import 'package:freezed_annotation/freezed_annotation.dart';424 425part 'physical_note.freezed.dart';426part 'physical_note.g.dart';427 428@freezed429class PhysicalNote with _$PhysicalNote {430 const factory PhysicalNote({431 required String id,432 required String subjectId,433 required String uniqueId,434 required String sourceName,435 required String pageNumber,436 String? sectionLabel,437 List<String>? tags,438 String? photoPath,439 String? voiceMemoPath,440 String? ocrText,441 String? locationShelf,442 String? locationContainer,443 String? qrCodeId,444 required DateTime createdAt,445 required DateTime updatedAt,446 DateTime? lastReviewedAt,447 @Default(0) int reviewCount,448 }) = _PhysicalNote;449 450 factory PhysicalNote.fromJson(Map<String, dynamic> json) =>451 _$PhysicalNoteFromJson(json);452}453```454 455## lib/models/review_item.dart456 457```dart458import 'package:freezed_annotation/freezed_annotation.dart';459 460part 'review_item.freezed.dart';461part 'review_item.g.dart';462 463enum CardState { newCard, learning, review, relearning }464 465@freezed466class ReviewItem with _$ReviewItem {467 const factory ReviewItem({468 required String id,469 required String noteId,470 required double difficulty,471 required double stability,472 required double elapsedDays,473 required double scheduledDays,474 @Default(0) int reps,475 @Default(0) int lapses,476 required CardState state,477 required DateTime due,478 @Default(false) bool suspended,479 @Default(false) bool leech,480 }) = _ReviewItem;481 482 factory ReviewItem.fromJson(Map<String, dynamic> json) =>483 _$ReviewItemFromJson(json);484}485```486 487## lib/models/review_log.dart488 489```dart490import 'package:freezed_annotation/freezed_annotation.dart';491 492part 'review_log.freezed.dart';493part 'review_log.g.dart';494 495enum Rating { again, hard, good, easy }496 497@freezed498class ReviewLog with _$ReviewLog {499 const factory ReviewLog({500 required String id,501 required String itemId,502 required Rating rating,503 required CardState state,504 required double elapsedDays,505 required double scheduledDays,506 int? reviewDuration,507 String? studySessionId,508 required DateTime createdAt,509 }) = _ReviewLog;510 511 factory ReviewLog.fromJson(Map<String, dynamic> json) =>512 _$ReviewLogFromJson(json);513}514```515 516## lib/models/study_session.dart517 518```dart519import 'package:freezed_annotation/freezed_annotation.dart';520 521part 'study_session.freezed.dart';522part 'study_session.g.dart';523 524enum SessionType { review, pomodoro, free, manual }525 526@freezed527class StudySession with _$StudySession {528 const factory StudySession({529 required String id,530 required String userId,531 required SessionType type,532 required DateTime startTime,533 DateTime? endTime,534 int? duration,535 int? itemsReviewed,536 Map<String, int>? ratings,537 double? retentionRate,538 String? subjectId,539 String? description,540 @Default('local') String syncStatus,541 }) = _StudySession;542 543 factory StudySession.fromJson(Map<String, dynamic> json) =>544 _$StudySessionFromJson(json);545}546```547 548## lib/models/exam.dart549 550```dart551import 'package:freezed_annotation/freezed_annotation.dart';552 553part 'exam.freezed.dart';554part 'exam.g.dart';555 556@freezed557class Exam with _$Exam {558 const factory Exam({559 required String id,560 required String userId,561 required String title,562 required DateTime date,563 required List<String> subjectIds,564 @Default(true) bool active,565 required DateTime createdAt,566 }) = _Exam;567 568 factory Exam.fromJson(Map<String, dynamic> json) => _$ExamFromJson(json);569}570```571 572## lib/models/user_settings.dart573 574```dart575import 'package:flutter/material.dart';576import 'package:freezed_annotation/freezed_annotation.dart';577 578part 'user_settings.freezed.dart';579part 'user_settings.g.dart';580 581@freezed582class UserSettings with _$UserSettings {583 const factory UserSettings({584 @Default('') String displayName,585 @Default('') String email,586 @Default(0.90) double requestRetention,587 @Default(36500) int maximumInterval,588 @Default(true) bool enableFuzz,589 @Default(150) int dailyReviewLimit,590 @Default(30) int dailyStudyGoalMinutes,591 @Default(false) bool useDyslexiaFont,592 @Default(ThemeMode.system) ThemeMode themeMode,593 @Default(true) bool enableNotifications,594 @Default('21:00') String reminderTime,595 @Default(7) int leechThreshold,596 @Default('en') String language,597 }) = _UserSettings;598 599 factory UserSettings.fromJson(Map<String, dynamic> json) =>600 _$UserSettingsFromJson(json);601}602```603 604## lib/database/tables.dart605 606```dart607import 'package:drift/drift.dart';608 609class Users extends Table {610 TextColumn get id => text()();611 TextColumn get displayName => text().nullable()();612 TextColumn get email => text().nullable()();613 IntColumn get createdAt => integer()();614 IntColumn get updatedAt => integer()();615 TextColumn get fsrsParams => text()();616 TextColumn get settings => text()();617 618 @override619 Set<Column> get primaryKey => {id};620}621 622class Subjects extends Table {623 TextColumn get id => text()();624 TextColumn get userId => text()();625 TextColumn get name => text()();626 IntColumn get color => integer()();627 TextColumn get icon => text().nullable()();628 TextColumn get description => text().nullable()();629 TextColumn get parentId => text().nullable()();630 TextColumn get fsrsParams => text().nullable()();631 IntColumn get createdAt => integer()();632 BoolColumn get archived => boolean().withDefault(const Constant(false))();633 634 @override635 Set<Column> get primaryKey => {id};636}637 638class PhysicalNotes extends Table {639 TextColumn get id => text()();640 TextColumn get subjectId => text()();641 TextColumn get uniqueId => text()();642 TextColumn get sourceName => text()();643 TextColumn get pageNumber => text()();644 TextColumn get sectionLabel => text().nullable()();645 TextColumn get tags => text().nullable()();646 TextColumn get photoPath => text().nullable()();647 TextColumn get voiceMemoPath => text().nullable()();648 TextColumn get ocrText => text().nullable()();649 TextColumn get locationShelf => text().nullable()();650 TextColumn get locationContainer => text().nullable()();651 TextColumn get qrCodeId => text().nullable()();652 IntColumn get createdAt => integer()();653 IntColumn get updatedAt => integer()();654 IntColumn get lastReviewedAt => integer().nullable()();655 IntColumn get reviewCount => integer().withDefault(const Constant(0))();656 657 @override658 Set<Column> get primaryKey => {id};659}660 661class ReviewItems extends Table {662 TextColumn get id => text()();663 TextColumn get noteId => text()();664 RealColumn get difficulty => real()();665 RealColumn get stability => real()();666 RealColumn get elapsedDays => real()();667 RealColumn get scheduledDays => real()();668 IntColumn get reps => integer().withDefault(const Constant(0))();669 IntColumn get lapses => integer().withDefault(const Constant(0))();670 IntColumn get state => integer()();671 IntColumn get due => integer()();672 BoolColumn get suspended => boolean().withDefault(const Constant(false))();673 BoolColumn get leech => boolean().withDefault(const Constant(false))();674 675 @override676 Set<Column> get primaryKey => {id};677}678 679class ReviewLogs extends Table {680 TextColumn get id => text()();681 TextColumn get itemId => text()();682 IntColumn get rating => integer()();683 IntColumn get state => integer()();684 RealColumn get elapsedDays => real()();685 RealColumn get scheduledDays => real()();686 IntColumn get reviewDuration => integer().nullable()();687 TextColumn get studySessionId => text().nullable()();688 IntColumn get createdAt => integer()();689 690 @override691 Set<Column> get primaryKey => {id};692}693 694class StudySessions extends Table {695 TextColumn get id => text()();696 TextColumn get userId => text()();697 TextColumn get type => text()();698 IntColumn get startTime => integer()();699 IntColumn get endTime => integer().nullable()();700 IntColumn get duration => integer().nullable()();701 IntColumn get itemsReviewed => integer().nullable()();702 TextColumn get ratingsJson => text().nullable()();703 RealColumn get retentionRate => real().nullable()();704 TextColumn get subjectId => text().nullable()();705 TextColumn get description => text().nullable()();706 TextColumn get syncStatus =>707 text().withDefault(const Constant('local'))();708 709 @override710 Set<Column> get primaryKey => {id};711}712 713class Exams extends Table {714 TextColumn get id => text()();715 TextColumn get userId => text()();716 TextColumn get title => text()();717 IntColumn get date => integer()();718 TextColumn get subjectsJson => text()();719 BoolColumn get active => boolean().withDefault(const Constant(true))();720 IntColumn get createdAt => integer()();721 722 @override723 Set<Column> get primaryKey => {id};724}725 726class Tags extends Table {727 TextColumn get id => text()();728 TextColumn get name => text().unique()();729 IntColumn get color => integer().nullable()();730 IntColumn get usageCount => integer().withDefault(const Constant(0))();731 732 @override733 Set<Column> get primaryKey => {id};734}735 736class NoteTags extends Table {737 TextColumn get noteId => text()();738 TextColumn get tagId => text()();739 740 @override741 Set<Column> get primaryKey => {noteId, tagId};742}743 744class SyncMetadata extends Table {745 IntColumn get id => integer()();746 IntColumn get lastSyncAt => integer().nullable()();747 TextColumn get syncToken => text().nullable()();748 TextColumn get deviceId => text()();749 750 @override751 Set<Column> get primaryKey => {id};752}753 754class PendingChanges extends Table {755 TextColumn get id => text()();756 TextColumn get tableName => text()();757 TextColumn get recordId => text()();758 TextColumn get operation => text()();759 TextColumn get payload => text()();760 IntColumn get createdAt => integer()();761 762 @override763 Set<Column> get primaryKey => {id};764}765```766 767## lib/database/database.dart768 769```dart770import 'dart:io';771import 'package:drift/drift.dart';772import 'package:drift_flutter/drift_flutter.dart';773import 'package:path_provider/path_provider.dart';774import 'package:path/path.dart' as p;775 776import 'tables.dart';777 778part 'database.g.dart';779 780@DriftDatabase(tables: [781 Users,782 Subjects,783 PhysicalNotes,784 ReviewItems,785 ReviewLogs,786 StudySessions,787 Exams,788 Tags,789 NoteTags,790 SyncMetadata,791 PendingChanges,792])793class AppDatabase extends _$AppDatabase {794 AppDatabase() : super(_openConnection());795 796 @override797 int get schemaVersion => 1;798 799 @override800 MigrationStrategy get migration => MigrationStrategy(801 onCreate: (Migrator m) async {802 await m.createAll();803 await _createIndexes(m);804 await _createFts5(m);805 },806 onUpgrade: (Migrator m, int from, int to) async {807 // Handle future migrations808 },809 );810 811 Future<void> _createIndexes(Migrator m) async {812 await m.createIndex(Index('idx_notes_subject',813 'CREATE INDEX idx_notes_subject ON physical_notes(subject_id)'));814 await m.createIndex(Index('idx_notes_unique',815 'CREATE INDEX idx_notes_unique ON physical_notes(unique_id)'));816 await m.createIndex(Index('idx_items_due',817 'CREATE INDEX idx_items_due ON review_items(due, suspended)'));818 await m.createIndex(Index('idx_logs_item',819 'CREATE INDEX idx_logs_item ON review_logs(item_id, created_at)'));820 await m.createIndex(Index('idx_sessions_time',821 'CREATE INDEX idx_sessions_time ON study_sessions(start_time)'));822 }823 824 Future<void> _createFts5(Migrator m) async {825 await customStatement('''826 CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(827 unique_id,828 source_name,829 section_label,830 ocr_text,831 content='physical_notes',832 content_rowid='rowid'833 )834 ''');835 }836 837 // Subject queries838 Future<List<Subject>> getAllSubjects() => select(subjects).get();839 840 Future<Subject?> getSubjectById(String id) =>841 (select(subjects)..where((s) => s.id.equals(id))).getSingleOrNull();842 843 Future<int> insertSubject(SubjectsCompanion subject) =>844 into(subjects).insert(subject);845 846 Future<bool> updateSubject(Subject subject) =>847 update(subjects).replace(subject);848 849 Future<int> deleteSubject(String id) =>850 (delete(subjects)..where((s) => s.id.equals(id))).go();851 852 // Note queries853 Future<List<PhysicalNote>> getNotesBySubject(String subjectId) =>854 (select(physicalNotes)..where((n) => n.subjectId.equals(subjectId))).get();855 856 Future<List<PhysicalNote>> getAllNotes() => select(physicalNotes).get();857 858 Future<PhysicalNote?> getNoteById(String id) =>859 (select(physicalNotes)..where((n) => n.id.equals(id))).getSingleOrNull();860 861 Future<int> insertNote(PhysicalNotesCompanion note) =>862 into(physicalNotes).insert(note);863 864 Future<bool> updateNote(PhysicalNote note) =>865 update(physicalNotes).replace(note);866 867 Future<int> deleteNote(String id) =>868 (delete(physicalNotes)..where((n) => n.id.equals(id))).go();869 870 // Review item queries871 Future<List<ReviewItem>> getDueItems(DateTime before) =>872 (select(reviewItems)873 ..where((i) => i.due.isSmallerOrEqualValue(before.millisecondsSinceEpoch))874 ..where((i) => i.suspended.equals(false)))875 .get();876 877 Future<List<ReviewItem>> getDueItemsBySubject(String subjectId, DateTime before) =>878 (select(reviewItems)879 ..where((i) => i.due.isSmallerOrEqualValue(before.millisecondsSinceEpoch))880 ..where((i) => i.suspended.equals(false)))881 .get(); // Note: needs join with notes for subject filter882 883 Future<ReviewItem?> getReviewItemForNote(String noteId) =>884 (select(reviewItems)..where((i) => i.noteId.equals(noteId))).getSingleOrNull();885 886 Future<int> insertReviewItem(ReviewItemsCompanion item) =>887 into(reviewItems).insert(item);888 889 Future<bool> updateReviewItem(ReviewItem item) =>890 update(reviewItems).replace(item);891 892 // Review log queries893 Future<int> insertReviewLog(ReviewLogsCompanion log) =>894 into(reviewLogs).insert(log);895 896 Future<List<ReviewLog>> getLogsForItem(String itemId) =>897 (select(reviewLogs)..where((l) => l.itemId.equals(itemId))).get();898 899 // Session queries900 Future<int> insertSession(StudySessionsCompanion session) =>901 into(studySessions).insert(session);902 903 Future<bool> updateSession(StudySession session) =>904 update(studySessions).replace(session);905 906 Future<List<StudySession>> getSessionsForDateRange(907 DateTime start, DateTime end) =>908 (select(studySessions)909 ..where((s) => s.startTime.isBiggerOrEqualValue(start.millisecondsSinceEpoch))910 ..where((s) => s.startTime.isSmallerOrEqualValue(end.millisecondsSinceEpoch)))911 .get();912 913 // Exam queries914 Future<List<Exam>> getActiveExams() =>915 (select(exams)..where((e) => e.active.equals(true))).get();916 917 Future<int> insertExam(ExamsCompanion exam) => into(exams).insert(exam);918 919 // Search920 Future<List<PhysicalNote>> searchNotes(String query) async {921 final ftsResults = await customSelect(922 'SELECT rowid FROM notes_fts WHERE notes_fts MATCH ?',923 variables: [Variable.withString(query)],924 ).get();925 926 final rowIds = ftsResults.map((r) => r.read<int>('rowid')).toList();927 if (rowIds.isEmpty) return [];928 929 // Fetch actual notes by rowid930 return (select(physicalNotes)931 ..where((n) => CustomExpression<bool>('rowid', rowIds as List<Expression>)))932 .get();933 }934 935 // Stats936 Future<int> getStudyMinutesForDate(DateTime date) async {937 final startOfDay = DateTime(date.year, date.month, date.day);938 final endOfDay = startOfDay.add(const Duration(days: 1));939 940 final result = await customSelect(941 'SELECT COALESCE(SUM(duration), 0) as total FROM study_sessions '942 'WHERE start_time >= ? AND start_time < ?',943 variables: [944 Variable.withInt(startOfDay.millisecondsSinceEpoch),945 Variable.withInt(endOfDay.millisecondsSinceEpoch),946 ],947 ).getSingle();948 949 return (result.read<double>('total') ?? 0) ~/ 60;950 }951 952 Future<Map<String, dynamic>> getReviewStats(DateTime start, DateTime end) async {953 final result = await customSelect(954 'SELECT rating, COUNT(*) as count FROM review_logs '955 'WHERE created_at >= ? AND created_at < ? '956 'GROUP BY rating',957 variables: [958 Variable.withInt(start.millisecondsSinceEpoch),959 Variable.withInt(end.millisecondsSinceEpoch),960 ],961 ).get();962 963 final stats = <String, int>{};964 for (final row in result) {965 stats[row.read<String>('rating')] = row.read<int>('count');966 }967 return stats;968 }969}970 971LazyDatabase _openConnection() {972 return LazyDatabase(() async {973 final dbFolder = await getApplicationDocumentsDirectory();974 final file = File(p.join(dbFolder.path, 'paperstudy.db'));975 return NativeDatabase.createInBackground(file);976 });977}978```979 980## lib/services/fsrs_service.dart981 982```dart983import 'dart:math';984import 'package:paperstudy/models/review_item.dart';985 986class FSRSCalculator {987 // FSRS-4.5 default weights988 static const List<double> defaultWeights = [989 0.40255, // w0990 0.74818, // w1991 0.93027, // w2992 2.26926, // w3993 5.06871, // w4994 4.93, // w5995 0.94, // w6996 0.86, // w7997 0.01, // w8998 1.49, // w9999 0.14, // w101000 0.94, // w111001 2.18, // w121002 0.05, // w131003 0.34, // w141004 1.26, // w151005 0.29, // w161006 2.61, // w171007 0.41, // w181008 0.96, // w191009 ];1010 1011 final List<double> w;1012 final double requestRetention;1013 final int maximumInterval;1014 final bool enableFuzz;1015 1016 FSRSCalculator({1017 List<double>? weights,1018 this.requestRetention = 0.90,1019 this.maximumInterval = 36500,1020 this.enableFuzz = true,1021 }) : w = weights ?? List.from(defaultWeights);1022 1023 double initialDifficulty(int rating) {1024 return w[4] - w[5] * (rating - 3) + 1;1025 }1026 1027 double initialStability(int rating) {1028 return max(w[rating], 0.1);1029 }1030 1031 double nextDifficulty(double d, int rating) {1032 final nextD = w[5] * initialDifficulty(3) +1033 (1 - w[5]) * (d - w[6] * (rating - 3));1034 return _constrainDifficulty(nextD);1035 }1036 1037 double nextStability(double s, double d, int rating, double r) {1038 final hardPenalty = rating == 2 ? w[12] : 1.0;1039 final easyBonus = rating == 4 ? w[13] : 1.0;1040 1041 double nextS;1042 if (rating == 1) {1043 nextS = w[7] * pow(d, -w[8]) * (pow(s + 1, w[19]) - 1) * exp((1 - r) * w[17]);1044 } else {1045 nextS = s *1046 (1 +1047 exp(w[9]) *1048 (11 - d) *1049 pow(s, -w[10]) *1050 hardPenalty *1051 easyBonus *1052 (exp((1 - r) * w[17]) - 1));1053 }1054 return max(nextS, 0.1);1055 }1056 1057 double retrievability(double elapsedDays, double stability) {1058 return pow(1 + elapsedDays / (9 * stability), -1).toDouble();1059 }1060 1061 int daysUntilNextReview(double stability, double requestRetention) {1062 final days = (9 * stability * (1 / requestRetention - 1)).ceil();1063 final fuzzed = enableFuzz ? _applyFuzz(days) : days;1064 return min(fuzzed, maximumInterval);1065 }1066 1067 int _applyFuzz(int interval) {1068 final random = Random();1069 final fuzzFactor = 0.05 + random.nextDouble() * 0.1; // 5-15% fuzz1070 final fuzz = (interval * fuzzFactor).round();1071 return interval + (random.nextBool() ? fuzz : -fuzz);1072 }1073 1074 double _constrainDifficulty(double d) => d.clamp(1.0, 10.0);1075 1076 // Process a review and return updated card state1077 ReviewResult processReview({1078 required ReviewItem item,1079 required int rating, // 1=Again, 2=Hard, 3=Good, 4=Easy1080 required DateTime now,1081 }) {1082 final elapsedDays = now.difference(item.due).inDays.toDouble();1083 final r = retrievability(elapsedDays, item.stability);1084 1085 final nextDifficulty = this.nextDifficulty(item.difficulty, rating);1086 final nextStability = this.nextStability(item.stability, item.difficulty, rating, r);1087 1088 CardState nextState;1089 int scheduledDays;1090 1091 switch (item.state) {1092 case CardState.newCard:1093 nextState = rating >= 3 ? CardState.review : CardState.learning;1094 scheduledDays = rating == 1 ? 1 : (rating == 2 ? 1 : (rating == 3 ? 2 : 4));1095 break;1096 case CardState.learning:1097 case CardState.relearning:1098 nextState = rating >= 3 ? CardState.review : item.state;1099 scheduledDays = rating == 1 ? 1 : daysUntilNextReview(nextStability, requestRetention);1100 break;1101 case CardState.review:1102 nextState = rating == 1 ? CardState.relearning : CardState.review;1103 scheduledDays = daysUntilNextReview(nextStability, requestRetention);1104 break;1105 }1106 1107 final nextDue = now.add(Duration(days: scheduledDays));1108 1109 return ReviewResult(1110 difficulty: nextDifficulty,1111 stability: nextStability,1112 state: nextState,1113 due: nextDue,1114 scheduledDays: scheduledDays.toDouble(),1115 elapsedDays: elapsedDays,1116 );1117 }1118}1119 1120class ReviewResult {1121 final double difficulty;1122 final double stability;1123 final CardState state;1124 final DateTime due;1125 final double scheduledDays;1126 final double elapsedDays;1127 1128 ReviewResult({1129 required this.difficulty,1130 required this.stability,1131 required this.state,1132 required this.due,1133 required this.scheduledDays,1134 required this.elapsedDays,1135 });1136}1137```1138 1139## lib/services/review_service.dart1140 1141```dart1142import 'package:flutter_riverpod/flutter_riverpod.dart';1143import 'package:paperstudy/database/database.dart';1144import 'package:paperstudy/models/review_item.dart';1145import 'package:paperstudy/models/review_log.dart';1146import 'package:paperstudy/services/fsrs_service.dart';1147import 'package:uuid/uuid.dart';1148 1149final reviewServiceProvider = Provider((ref) => ReviewService(ref));1150 1151class ReviewService {1152 final ProviderRef ref;1153 final _uuid = const Uuid();1154 late final AppDatabase _db;1155 late final FSRSCalculator _fsrs;1156 1157 ReviewService(this.ref) {1158 _db = ref.read(databaseProvider);1159 _fsrs = FSRSCalculator();1160 }1161 1162 // Get today's review queue1163 Future<List<ReviewQueueItem>> getReviewQueue({String? subjectId}) async {1164 final now = DateTime.now();1165 final items = subjectId != null1166 ? await _db.getDueItemsBySubject(subjectId, now)1167 : await _db.getDueItems(now);1168 1169 final queue = <ReviewQueueItem>[];1170 for (final item in items) {1171 final note = await _db.getNoteById(item.noteId);1172 if (note != null) {1173 queue.add(ReviewQueueItem(1174 reviewItem: item,1175 note: note,1176 ));1177 }1178 }1179 return queue;1180 }1181 1182 // Process a rating1183 Future<void> processRating({1184 required String itemId,1185 required int rating,1186 required DateTime now,1187 String? sessionId,1188 }) async {1189 final item = await _db.getReviewItemForNote(itemId) ??1190 await _db.getReviewItemById(itemId);1191 if (item == null) throw Exception('Review item not found');1192 1193 final result = _fsrs.processReview(1194 item: item,1195 rating: rating,1196 now: now,1197 );1198 1199 // Update review item1200 final updatedItem = item.copyWith(