import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:pdfrx/pdfrx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'editor/pdf/pen_capture_region.dart'; import 'l10n/app_localizations.dart'; import 'providers/settings_provider.dart'; import 'screens/home_screen.dart'; import 'screens/vault_setup_screen.dart'; import 'services/database_service.dart'; import 'services/vault_service.dart'; Future main() async { // Kind-aware binding (extends WidgetsFlutterBinding) must be the active // binding before runApp so the M1 spike's PenCaptureRegion can gate // hit-testing by pointer kind. Safe for the rest of the app: with no pen // region mounted it behaves exactly like the default binding. PenCaptureBinding.ensureInitialized(); // pdfrx native engine init (required before any PdfViewer is built). pdfrxFlutterInitialize(); // Ensure DB is ready before the app starts so providers can use it eagerly await DatabaseService.getInstance(); // Initialize SharedPreferences await SharedPreferences.getInstance(); runApp(const ProviderScope(child: BadNoteApp())); } class BadNoteApp extends ConsumerWidget { const BadNoteApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(settingsProvider); // Material You: prefer the OS dynamic color (Windows/Android system accent); // fall back to the user's seed color when the platform provides none. return DynamicColorBuilder( builder: (lightDynamic, darkDynamic) { final lightScheme = lightDynamic?.harmonized() ?? ColorScheme.fromSeed( seedColor: settings.colorSchemeSeed, brightness: Brightness.light, ); final darkScheme = darkDynamic?.harmonized() ?? ColorScheme.fromSeed( seedColor: settings.colorSchemeSeed, brightness: Brightness.dark, ); return MaterialApp( title: 'BadNote', themeMode: settings.themeMode, theme: _theme(lightScheme), darkTheme: _theme(darkScheme), // i18n: follows the OS language (en / zh) via the system locale. localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, home: const VaultGate(), ); }, ); } ThemeData _theme(ColorScheme scheme) => ThemeData( colorScheme: scheme, useMaterial3: true, textTheme: GoogleFonts.interTextTheme( ThemeData(brightness: scheme.brightness).textTheme, ), ); } /// Startup gate: shows [HomeScreen] only once a valid vault root folder has been /// chosen. If none is set — or the saved folder no longer exists — it shows /// [VaultSetupScreen] first (re-prompting on a missing folder rather than /// silently scattering data elsewhere). Phase 0: records the vault path only; /// editors still use SQLite. class VaultGate extends StatefulWidget { const VaultGate({super.key}); @override State createState() => _VaultGateState(); } class _VaultGateState extends State { VaultService? _vault; bool _valid = false; bool _hadStoredPath = false; bool _loading = true; @override void initState() { super.initState(); _check(); } Future _check() async { final vault = await VaultService.getInstance(); final valid = await vault.vaultRootValid(); if (!mounted) return; setState(() { _vault = vault; _valid = valid; // A stored-but-invalid path means the chosen folder went missing. _hadStoredPath = (vault.vaultRoot?.isNotEmpty ?? false); _loading = false; }); } @override Widget build(BuildContext context) { if (_loading) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } if (_valid) return const HomeScreen(); return VaultSetupScreen( vaultService: _vault!, missing: _hadStoredPath, onVaultReady: () => setState(() => _valid = true), ); } }