Team Ai
Apppublic

Frozenhell/paperstudy-flutter-app-source-code

sourceHugging Faceupdated 26d agoView on Hugging Face
0likes
index.html5152 linesDownload Raw Back to root
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(

Showing the first 1,200 of 5152 lines. Download the file for the rest.