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'
26 import 'package:pve_flutter_frontend/utils/proxmox_layout_builder.dart';
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';
36 WidgetsFlutterBinding.ensureInitialized();
37 final authBloc = PveAuthenticationBloc();
39 final loginStorage = await (ProxmoxLoginStorage.fromLocalStorage());
40 final apiClient = await loginStorage!.recoverLatestSession();
41 authBloc.events.add(LoggedIn(apiClient));
46 authBloc.events.add(LoggedOut());
49 ProxmoxGlobalErrorBloc();
50 FlutterError.onError = (FlutterErrorDetails details) {
51 FlutterError.dumpErrorToConsole(details);
52 if (kReleaseMode) ProxmoxGlobalErrorBloc().addError(details.exception);
54 SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
55 Provider.debugCheckInvalidValueType = null;
60 Provider.value(value: authBloc),
61 Provider<PveResourceBloc>(
62 create: (context) => PveResourceBloc(init: PveResourceState.init()),
63 dispose: (context, bloc) => bloc.dispose(),
68 sharedPreferences: sharedPreferences,
74 class MyApp extends StatelessWidget {
75 final PveAuthenticationBloc? authbloc;
76 final SharedPreferences sharedPreferences;
77 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
79 MyApp({super.key, this.authbloc, required this.sharedPreferences});
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());
91 if (state is Unauthenticated) {
92 Provider.of<PveResourceBloc>(context, listen: false).apiClient = null;
96 navigatorKey: navigatorKey,
98 //themeMode: ThemeMode.dark, // comment in/out to test
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,
112 indicatorColor: ProxmoxColors.orange,
113 textButtonTheme: TextButtonThemeData(
114 style: TextButton.styleFrom(foregroundColor: ProxmoxColors.grey),
116 outlinedButtonTheme: OutlinedButtonThemeData(
117 style: OutlinedButton.styleFrom(
118 foregroundColor: Colors.black,
121 fontFamily: "Open Sans",
122 primaryTextTheme: const TextTheme(
124 TextStyle(fontFamily: "Open Sans", fontWeight: FontWeight.w700),
126 appBarTheme: const AppBarTheme(
127 backgroundColor: ProxmoxColors.supportBlue, // primary
128 foregroundColor: Colors.white, // onPrimary
130 textSelectionTheme: TextSelectionThemeData(
131 selectionColor: ProxmoxColors.orange.withOpacity(0.4),
132 selectionHandleColor: ProxmoxColors.orange,
133 cursorColor: ProxmoxColors.orange,
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,
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
156 textButtonTheme: TextButtonThemeData(
158 TextButton.styleFrom(foregroundColor: ProxmoxColors.greyTint80),
160 outlinedButtonTheme: OutlinedButtonThemeData(
161 style: OutlinedButton.styleFrom(
162 foregroundColor: Colors.white,
165 fontFamily: "Open Sans",
166 primaryTextTheme: const TextTheme(
168 TextStyle(fontFamily: "Open Sans", fontWeight: FontWeight.w700),
170 scaffoldBackgroundColor: ProxmoxColors.grey,
171 textSelectionTheme: TextSelectionThemeData(
172 selectionColor: ProxmoxColors.orange.withOpacity(0.4),
173 selectionHandleColor: ProxmoxColors.orange,
174 cursorColor: ProxmoxColors.orange,
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;
184 await showDialog<String>(
185 context: navigatorKey.currentState!.overlay!.context,
186 builder: (BuildContext context) {
187 return StreamBuilder<Object>(
188 stream: ProxmoxGlobalErrorBloc().onError,
190 builder: (context, snapshot) {
192 contentPadding: const EdgeInsets.fromLTRB(
193 24.0, 12.0, 24.0, 16.0),
195 mainAxisAlignment: MainAxisAlignment.spaceBetween,
201 content: SingleChildScrollView(
202 child: Text(snapshot.data?.toString() ?? ''),
208 ProxmoxGlobalErrorBloc().dialogVisible = false;
214 onGenerateRoute: (context) {
215 if (authbloc!.state.value is Uninitialized) {
216 return MaterialPageRoute(
217 builder: (context) => const PveSplashScreen(),
220 if (sharedPreferences.getBool('showWelcomeScreen') ?? true) {
221 return MaterialPageRoute(
222 builder: (context) => const PveWelcome(),
226 if (authbloc!.state.value is Unauthenticated ||
227 context.name == '/login') {
228 return MaterialPageRoute(
230 return StreamListener<PveAuthenticationState>(
231 stream: authbloc!.state,
232 onStateChange: (state) {
233 if (state is Authenticated) {
234 Navigator.of(context).pushReplacementNamed('/');
237 child: ProxmoxLoginSelector(
238 onLogin: (client) => authbloc!.events.add(LoggedIn(client)),
244 if (authbloc!.state.value is Authenticated) {
245 final state = authbloc!.state.value as Authenticated;
246 if (PveQemuOverview.routeName.hasMatch(context.name!)) {
248 PveQemuOverview.routeName.firstMatch(context.name!)!;
249 final String nodeID = match.group(1)!;
250 final String guestID = match.group(2)!;
252 return MaterialPageRoute(
253 fullscreenDialog: false,
256 return MultiProvider(
258 Provider<PveQemuOverviewBloc>(
259 create: (context) => PveQemuOverviewBloc(
261 apiClient: state.apiClient,
262 init: PveQemuOverviewState.init(nodeID),
263 )..events.add(UpdateQemuStatus()),
264 dispose: (context, bloc) => bloc.dispose(),
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(),
275 child: PveQemuOverview(
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)!;
287 return MaterialPageRoute(
288 fullscreenDialog: false,
291 return MultiProvider(
293 Provider<PveLxcOverviewBloc>(
294 create: (context) => PveLxcOverviewBloc(
296 apiClient: state.apiClient,
297 init: PveLxcOverviewState.init(nodeID),
298 )..events.add(UpdateLxcStatus()),
299 dispose: (context, bloc) => bloc.dispose(),
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(),
310 child: PveLxcOverview(
318 if (PveNodeOverview.routeName.hasMatch(context.name!)) {
320 PveNodeOverview.routeName.firstMatch(context.name!)!;
321 final String nodeID = match.group(1)!;
322 return MaterialPageRoute(
323 fullscreenDialog: false,
326 final rbloc = Provider.of<PveResourceBloc>(context);
327 return MultiProvider(
329 Provider<PveNodeOverviewBloc>(
330 create: (context) => PveNodeOverviewBloc(
331 apiClient: state.apiClient,
333 init: PveNodeOverviewState.init(
334 rbloc.latestState.isStandalone),
335 )..events.add(UpdateNodeStatus()),
336 dispose: (context, bloc) => bloc.dispose(),
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(),
346 child: PveNodeOverview(
353 switch (context.name) {
355 return MaterialPageRoute(
356 fullscreenDialog: true,
358 builder: (context) => MultiProvider(
360 Provider<proxclient.ProxmoxApiClient>.value(
361 value: state.apiClient,
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(),
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(),
381 return MaterialPageRoute(
384 return const NotFoundPage();
389 return MaterialPageRoute(
392 return const NotFoundPage();