]> git.proxmox.com Git - flutter/pve_flutter_frontend.git/blob - lib/main.dart
theme: drop checkbox/radiogroup related overrides
[flutter/pve_flutter_frontend.git] / lib / main.dart
1 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart';
3 import 'package:provider/provider.dart';
4 import 'package:pve_flutter_frontend/widgets/pve_first_welcome_screen.dart';
5 import 'package:shared_preferences/shared_preferences.dart';
6 import 'package:proxmox_login_manager/proxmox_login_manager.dart';
7 import 'package:pve_flutter_frontend/bloc/pve_authentication_bloc.dart';
8 import 'package:pve_flutter_frontend/bloc/pve_cluster_status_bloc.dart';
9 import 'package:pve_flutter_frontend/bloc/pve_lxc_overview_bloc.dart';
10 import 'package:pve_flutter_frontend/bloc/pve_node_overview_bloc.dart';
11 import 'package:pve_flutter_frontend/bloc/pve_qemu_overview_bloc.dart';
12 import 'package:pve_flutter_frontend/bloc/pve_resource_bloc.dart';
13 import 'package:pve_flutter_frontend/bloc/pve_task_log_bloc.dart';
14 import 'package:pve_flutter_frontend/pages/404_page.dart';
15 import 'package:pve_flutter_frontend/pages/main_layout_slim.dart';
16 import 'package:pve_flutter_frontend/states/pve_cluster_status_state.dart';
17 import 'package:pve_flutter_frontend/states/pve_lxc_overview_state.dart';
18 import 'package:pve_flutter_frontend/states/pve_node_overview_state.dart';
19 import 'package:pve_flutter_frontend/states/pve_qemu_overview_state.dart';
20 import 'package:pve_flutter_frontend/states/pve_resource_state.dart';
21 import 'package:pve_flutter_frontend/states/pve_task_log_state.dart';
22 import 'package:pve_flutter_frontend/widgets/proxmox_stream_listener.dart';
23 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
24 as proxclient;
25
26 import 'package:pve_flutter_frontend/utils/proxmox_layout_builder.dart';
27
28 import 'package:pve_flutter_frontend/bloc/proxmox_global_error_bloc.dart';
29 import 'package:pve_flutter_frontend/widgets/pve_lxc_overview.dart';
30 import 'package:pve_flutter_frontend/widgets/pve_node_overview.dart';
31 import 'package:pve_flutter_frontend/widgets/pve_qemu_overview.dart';
32 import 'package:pve_flutter_frontend/widgets/pve_splash_screen.dart';
33 import 'package:pve_flutter_frontend/utils/proxmox_colors.dart';
34
35 void main() async {
36 WidgetsFlutterBinding.ensureInitialized();
37 final authBloc = PveAuthenticationBloc();
38 try {
39 final loginStorage = await (ProxmoxLoginStorage.fromLocalStorage());
40 final apiClient = await loginStorage!.recoverLatestSession();
41 authBloc.events.add(LoggedIn(apiClient));
42 } catch (e) {
43 if (kDebugMode) {
44 print(e);
45 }
46 authBloc.events.add(LoggedOut());
47 }
48
49 ProxmoxGlobalErrorBloc();
50 FlutterError.onError = (FlutterErrorDetails details) {
51 FlutterError.dumpErrorToConsole(details);
52 if (kReleaseMode) ProxmoxGlobalErrorBloc().addError(details.exception);
53 };
54 SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
55 Provider.debugCheckInvalidValueType = null;
56
57 runApp(
58 MultiProvider(
59 providers: [
60 Provider.value(value: authBloc),
61 Provider<PveResourceBloc>(
62 create: (context) => PveResourceBloc(init: PveResourceState.init()),
63 dispose: (context, bloc) => bloc.dispose(),
64 ),
65 ],
66 child: MyApp(
67 authbloc: authBloc,
68 sharedPreferences: sharedPreferences,
69 ),
70 ),
71 );
72 }
73
74 class MyApp extends StatelessWidget {
75 final PveAuthenticationBloc? authbloc;
76 final SharedPreferences sharedPreferences;
77 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
78
79 MyApp({super.key, this.authbloc, required this.sharedPreferences});
80
81 @override
82 Widget build(BuildContext context) {
83 return StreamListener<PveAuthenticationState>(
84 stream: authbloc!.state,
85 onStateChange: (state) {
86 if (state is Authenticated) {
87 Provider.of<PveResourceBloc>(context, listen: false)
88 ..apiClient = state.apiClient
89 ..events.add(PollResources());
90 }
91 if (state is Unauthenticated) {
92 Provider.of<PveResourceBloc>(context, listen: false).apiClient = null;
93 }
94 },
95 child: MaterialApp(
96 navigatorKey: navigatorKey,
97 title: 'Proxmox',
98 //themeMode: ThemeMode.dark, // comment in/out to test
99 theme: ThemeData(
100 colorScheme: const ColorScheme.light(
101 brightness: Brightness.light,
102 primary: ProxmoxColors.supportBlue,
103 onPrimary: Colors.white,
104 primaryContainer: ProxmoxColors.blue900,
105 secondary: ProxmoxColors.orange,
106 secondaryContainer: ProxmoxColors.supportLightOrange,
107 surface: ProxmoxColors.supportGreyTint50,
108 onSurface: Colors.black,
109 background: ProxmoxColors.supportGreyTint75,
110 onBackground: Colors.black,
111 ),
112 indicatorColor: ProxmoxColors.orange,
113 textButtonTheme: TextButtonThemeData(
114 style: TextButton.styleFrom(foregroundColor: ProxmoxColors.grey),
115 ),
116 outlinedButtonTheme: OutlinedButtonThemeData(
117 style: OutlinedButton.styleFrom(
118 foregroundColor: Colors.black,
119 ),
120 ),
121 fontFamily: "Open Sans",
122 primaryTextTheme: const TextTheme(
123 titleLarge:
124 TextStyle(fontFamily: "Open Sans", fontWeight: FontWeight.w700),
125 ),
126 appBarTheme: const AppBarTheme(
127 backgroundColor: ProxmoxColors.supportBlue, // primary
128 foregroundColor: Colors.white, // onPrimary
129 ),
130 textSelectionTheme: TextSelectionThemeData(
131 selectionColor: ProxmoxColors.orange.withOpacity(0.4),
132 selectionHandleColor: ProxmoxColors.orange,
133 cursorColor: ProxmoxColors.orange,
134 ),
135 ),
136 darkTheme: ThemeData(
137 colorScheme: const ColorScheme.dark(
138 brightness: Brightness.dark,
139 primary: ProxmoxColors.supportBlue,
140 onPrimary: Colors.white,
141 primaryContainer: ProxmoxColors.blue800,
142 surface: ProxmoxColors.greyTint20,
143 onSurface: Colors.white,
144 secondary: ProxmoxColors.orange,
145 secondaryContainer: ProxmoxColors.supportLightOrange,
146 background: ProxmoxColors.grey,
147 onBackground: ProxmoxColors.supportGreyTint75,
148 ),
149 indicatorColor: ProxmoxColors.orange,
150 // flutter has a weird logic where it pulls colors from different
151 // scheme properties depending on light/dark mode, avoid that...
152 appBarTheme: const AppBarTheme(
153 backgroundColor: ProxmoxColors.supportBlue, // primary
154 foregroundColor: Colors.white, // onPrimary
155 ),
156 textButtonTheme: TextButtonThemeData(
157 style:
158 TextButton.styleFrom(foregroundColor: ProxmoxColors.greyTint80),
159 ),
160 outlinedButtonTheme: OutlinedButtonThemeData(
161 style: OutlinedButton.styleFrom(
162 foregroundColor: Colors.white,
163 ),
164 ),
165 fontFamily: "Open Sans",
166 primaryTextTheme: const TextTheme(
167 titleLarge:
168 TextStyle(fontFamily: "Open Sans", fontWeight: FontWeight.w700),
169 ),
170 scaffoldBackgroundColor: ProxmoxColors.grey,
171 textSelectionTheme: TextSelectionThemeData(
172 selectionColor: ProxmoxColors.orange.withOpacity(0.4),
173 selectionHandleColor: ProxmoxColors.orange,
174 cursorColor: ProxmoxColors.orange,
175 ),
176 ),
177 builder: (context, child) {
178 return StreamListener(
179 stream: ProxmoxGlobalErrorBloc().onError.distinct(),
180 onStateChange: (dynamic error) async {
181 if (!ProxmoxGlobalErrorBloc().dialogVisible) {
182 ProxmoxGlobalErrorBloc().dialogVisible = true;
183
184 await showDialog<String>(
185 context: navigatorKey.currentState!.overlay!.context,
186 builder: (BuildContext context) {
187 return StreamBuilder<Object>(
188 stream: ProxmoxGlobalErrorBloc().onError,
189 initialData: error,
190 builder: (context, snapshot) {
191 return AlertDialog(
192 contentPadding: const EdgeInsets.fromLTRB(
193 24.0, 12.0, 24.0, 16.0),
194 title: const Row(
195 mainAxisAlignment: MainAxisAlignment.spaceBetween,
196 children: <Widget>[
197 Text('Error'),
198 Icon(Icons.warning),
199 ],
200 ),
201 content: SingleChildScrollView(
202 child: Text(snapshot.data?.toString() ?? ''),
203 ),
204 );
205 });
206 },
207 );
208 ProxmoxGlobalErrorBloc().dialogVisible = false;
209 }
210 },
211 child: child,
212 );
213 },
214 onGenerateRoute: (context) {
215 if (authbloc!.state.value is Uninitialized) {
216 return MaterialPageRoute(
217 builder: (context) => const PveSplashScreen(),
218 );
219 }
220 if (sharedPreferences.getBool('showWelcomeScreen') ?? true) {
221 return MaterialPageRoute(
222 builder: (context) => const PveWelcome(),
223 );
224 }
225
226 if (authbloc!.state.value is Unauthenticated ||
227 context.name == '/login') {
228 return MaterialPageRoute(
229 builder: (context) {
230 return StreamListener<PveAuthenticationState>(
231 stream: authbloc!.state,
232 onStateChange: (state) {
233 if (state is Authenticated) {
234 Navigator.of(context).pushReplacementNamed('/');
235 }
236 },
237 child: ProxmoxLoginSelector(
238 onLogin: (client) => authbloc!.events.add(LoggedIn(client)),
239 ),
240 );
241 },
242 );
243 }
244 if (authbloc!.state.value is Authenticated) {
245 final state = authbloc!.state.value as Authenticated;
246 if (PveQemuOverview.routeName.hasMatch(context.name!)) {
247 final match =
248 PveQemuOverview.routeName.firstMatch(context.name!)!;
249 final String nodeID = match.group(1)!;
250 final String guestID = match.group(2)!;
251
252 return MaterialPageRoute(
253 fullscreenDialog: false,
254 settings: context,
255 builder: (_) {
256 return MultiProvider(
257 providers: [
258 Provider<PveQemuOverviewBloc>(
259 create: (context) => PveQemuOverviewBloc(
260 guestID: guestID,
261 apiClient: state.apiClient,
262 init: PveQemuOverviewState.init(nodeID),
263 )..events.add(UpdateQemuStatus()),
264 dispose: (context, bloc) => bloc.dispose(),
265 ),
266 Provider<PveTaskLogBloc>(
267 create: (context) => PveTaskLogBloc(
268 apiClient: state.apiClient,
269 init: PveTaskLogState.init(nodeID))
270 ..events.add(FilterTasksByGuestID(guestID: guestID))
271 ..events.add(LoadTasks()),
272 dispose: (context, bloc) => bloc.dispose(),
273 )
274 ],
275 child: PveQemuOverview(
276 guestID: guestID,
277 ),
278 );
279 },
280 );
281 }
282 if (PveLxcOverview.routeName.hasMatch(context.name!)) {
283 final match = PveLxcOverview.routeName.firstMatch(context.name!)!;
284 final String nodeID = match.group(1)!;
285 final String guestID = match.group(2)!;
286
287 return MaterialPageRoute(
288 fullscreenDialog: false,
289 settings: context,
290 builder: (_) {
291 return MultiProvider(
292 providers: [
293 Provider<PveLxcOverviewBloc>(
294 create: (context) => PveLxcOverviewBloc(
295 guestID: guestID,
296 apiClient: state.apiClient,
297 init: PveLxcOverviewState.init(nodeID),
298 )..events.add(UpdateLxcStatus()),
299 dispose: (context, bloc) => bloc.dispose(),
300 ),
301 Provider<PveTaskLogBloc>(
302 create: (context) => PveTaskLogBloc(
303 apiClient: state.apiClient,
304 init: PveTaskLogState.init(nodeID))
305 ..events.add(FilterTasksByGuestID(guestID: guestID))
306 ..events.add(LoadTasks()),
307 dispose: (context, bloc) => bloc.dispose(),
308 )
309 ],
310 child: PveLxcOverview(
311 guestID: guestID,
312 ),
313 );
314 },
315 );
316 }
317
318 if (PveNodeOverview.routeName.hasMatch(context.name!)) {
319 final match =
320 PveNodeOverview.routeName.firstMatch(context.name!)!;
321 final String nodeID = match.group(1)!;
322 return MaterialPageRoute(
323 fullscreenDialog: false,
324 settings: context,
325 builder: (context) {
326 final rbloc = Provider.of<PveResourceBloc>(context);
327 return MultiProvider(
328 providers: [
329 Provider<PveNodeOverviewBloc>(
330 create: (context) => PveNodeOverviewBloc(
331 apiClient: state.apiClient,
332 nodeID: nodeID,
333 init: PveNodeOverviewState.init(
334 rbloc.latestState.isStandalone),
335 )..events.add(UpdateNodeStatus()),
336 dispose: (context, bloc) => bloc.dispose(),
337 ),
338 Provider<PveTaskLogBloc>(
339 create: (context) => PveTaskLogBloc(
340 apiClient: state.apiClient,
341 init: PveTaskLogState.init(nodeID))
342 ..events.add(LoadTasks()),
343 dispose: (context, bloc) => bloc.dispose(),
344 )
345 ],
346 child: PveNodeOverview(
347 nodeID: nodeID,
348 ),
349 );
350 },
351 );
352 }
353 switch (context.name) {
354 case '/':
355 return MaterialPageRoute(
356 fullscreenDialog: true,
357 settings: context,
358 builder: (context) => MultiProvider(
359 providers: [
360 Provider<proxclient.ProxmoxApiClient>.value(
361 value: state.apiClient,
362 ),
363 Provider<PveClusterStatusBloc>(
364 create: (context) => PveClusterStatusBloc(
365 apiClient: state.apiClient,
366 init: PveClusterStatusState.init())
367 ..events.add(UpdateClusterStatus()),
368 dispose: (context, bloc) => bloc.dispose(),
369 )
370 ],
371 //TODO add a wide layout option here when it's ready
372 child: ProxmoxLayoutBuilder(
373 builder: (context, layout) => layout != ProxmoxLayout.slim
374 ? const MainLayoutSlim()
375 : const MainLayoutSlim(),
376 ),
377 ),
378 );
379 break;
380 default:
381 return MaterialPageRoute(
382 settings: context,
383 builder: (context) {
384 return const NotFoundPage();
385 },
386 );
387 }
388 }
389 return MaterialPageRoute(
390 settings: context,
391 builder: (context) {
392 return const NotFoundPage();
393 },
394 );
395 },
396 initialRoute: '/',
397 ),
398 );
399 }
400 }