main
dart 165 lines 5.09 KB
Raw
1 import 'dart:convert';
2
3 import 'package:flutter/material.dart';
4 import 'package:flutter_riverpod/flutter_riverpod.dart';
5 import 'package:shared_preferences/shared_preferences.dart';
6
7 import '../app.dart';
8 import '../core/providers/app_providers.dart';
9 import '../core/providers/highlighter_provider.dart';
10 import '../core/providers/storage_providers.dart';
11 import '../core/storage/objectbox_store.dart';
12 import '../features/on_device/data/on_device_gemma_service.dart';
13 import '../features/on_device/providers/on_device_providers.dart';
14 import '../core/models/enums.dart';
15 import '../features/personas/providers/personas_providers.dart';
16 import '../features/servers/data/models/server.dart';
17 import '../features/servers/providers/server_providers.dart';
18 import '../features/cloud_sync/views/cloud_sync_lifecycle_host.dart';
19 import '../core/logger/app_logger.dart';
20 import '../core/utils/locale_utils.dart';
21 import 'bootstrap_screen.dart';
22 import 'bootstrap_state.dart';
23 import 'safe_riverpod_scope_host.dart';
24
25 class BootstrapHost extends StatefulWidget {
26 const BootstrapHost({super.key});
27
28 @override
29 State<BootstrapHost> createState() => _BootstrapHostState();
30 }
31
32 class _BootstrapHostState extends State<BootstrapHost> {
33 BootstrapState _state = const BootstrapState();
34 ProviderContainer? _container;
35 Locale? _savedLocale;
36
37 @override
38 void initState() {
39 super.initState();
40 _runBootstrap();
41 }
42
43 Future<void> _runBootstrap() async {
44 _updateStage(BootstrapStage.initializing, 'Initializing...');
45
46 try {
47 final results = await Future.wait([
48 initializeHighlighter(),
49 SharedPreferences.getInstance(),
50 ObjectBoxStore.create(),
51 ]);
52
53 final prefs = results[1] as SharedPreferences;
54 final database = results[2] as ObjectBoxStore;
55
56 // Seed built-in personas once before any provider reads the database.
57 PersonasNotifier.seedIfNeeded(database);
58
59 _updateStage(BootstrapStage.preparingApp, 'Preparing app...');
60
61 try {
62 final settingsJson = prefs.getString('appSettings');
63 if (settingsJson != null) {
64 final settings = json.decode(settingsJson) as Map<String, dynamic>;
65 final code = settings['localeCode'] as String?;
66 _savedLocale = parseLocaleCode(code);
67 }
68 } catch (_) {}
69
70 final container = ProviderContainer(
71 overrides: [
72 sharedPreferencesProvider.overrideWithValue(prefs),
73 databaseProvider.overrideWithValue(database),
74 ],
75 );
76
77 _updateStage(
78 BootstrapStage.initializingServices,
79 'Initializing services...',
80 );
81
82 final hfToken = container.read(
83 settingsProvider.select((s) => s.huggingFaceToken),
84 );
85 await OnDeviceGemmaService.initialize(huggingFaceToken: hfToken);
86
87 // Migrate models from old custom download location in background to prevent cold start ANR
88 OnDeviceGemmaService.migrateOldModels().catchError((e) {
89 Log.error('Error migrating old models: $e');
90 return 0;
91 });
92
93 _updateStage(BootstrapStage.configuringServer, 'Configuring server...');
94 final servers = await container.read(serversProvider.future);
95 final hasOnDevice = servers.any(
96 (s) => s.type == ServerType.onDevice || s.id == 'on-device',
97 );
98 if (!hasOnDevice) {
99 final server = Server(
100 id: 'on-device',
101 name: 'On-Device',
102 type: ServerType.onDevice,
103 host: '',
104 port: 0,
105 isDefault: false,
106 createdAt: DateTime.now(),
107 lastConnectedAt: DateTime.now(),
108 status: ConnectionStatus.connected,
109 iconName: 'strokeRoundedSmartPhone01',
110 );
111 await container.read(serversProvider.notifier).addServer(server);
112 }
113
114 // Eagerly resolve foundational state providers so their initial state
115 // is stable before UncontrolledProviderScope mounts the widget tree.
116 container.read(settingsProvider);
117 container.read(activeServerIdProvider);
118 container.read(activeServerProvider);
119 container.read(connectionStatusProvider);
120 container.read(onDeviceEngineProvider);
121
122 _container = container;
123 _updateStage(BootstrapStage.done, 'Ready');
124 } catch (e) {
125 _state = BootstrapState(
126 stage: BootstrapStage.error,
127 statusMessage: 'Startup failed',
128 error: e,
129 );
130 if (mounted) setState(() {});
131 }
132 }
133
134 void _updateStage(BootstrapStage stage, String message) {
135 if (!mounted) return;
136 setState(() {
137 _state = BootstrapState(stage: stage, statusMessage: message);
138 });
139 }
140
141 @override
142 void dispose() {
143 _container?.dispose();
144 super.dispose();
145 }
146
147 @override
148 Widget build(BuildContext context) {
149 if (_container != null) {
150 return UncontrolledProviderScope(
151 container: _container!,
152 child: SafeRiverpodScopeHost(
153 container: _container!,
154 child: const CloudSyncLifecycleHost(child: App()),
155 ),
156 );
157 }
158
159 return BootstrapScreen(
160 state: _state,
161 locale: _savedLocale,
162 onRetry: _state.stage == BootstrapStage.error ? _runBootstrap : null,
163 );
164 }
165 }