316 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:io';
 | |
| 
 | |
| import 'package:auto_route/auto_route.dart';
 | |
| import 'package:bitsdojo_window/bitsdojo_window.dart';
 | |
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/foundation.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:island/pods/userinfo.dart';
 | |
| import 'package:island/pods/websocket.dart';
 | |
| import 'package:island/route.dart';
 | |
| import 'package:material_symbols_icons/material_symbols_icons.dart';
 | |
| import 'package:path_provider/path_provider.dart';
 | |
| import 'package:responsive_framework/responsive_framework.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| 
 | |
| class WindowScaffold extends HookConsumerWidget {
 | |
|   final Widget child;
 | |
|   final AppRouter router;
 | |
|   const WindowScaffold({super.key, required this.child, required this.router});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     if (!kIsWeb &&
 | |
|         (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
 | |
|       final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
 | |
|       final windowButtonColor = WindowButtonColors(
 | |
|         iconNormal: Theme.of(context).colorScheme.primary,
 | |
|         mouseOver: Theme.of(context).colorScheme.primaryContainer,
 | |
|         mouseDown: Theme.of(context).colorScheme.onPrimaryContainer,
 | |
|         iconMouseOver: Theme.of(context).colorScheme.primary,
 | |
|         iconMouseDown: Theme.of(context).colorScheme.primary,
 | |
|       );
 | |
| 
 | |
|       return Material(
 | |
|         child: Stack(
 | |
|           fit: StackFit.expand,
 | |
|           children: [
 | |
|             Column(
 | |
|               children: [
 | |
|                 WindowTitleBarBox(
 | |
|                   child: Container(
 | |
|                     decoration: BoxDecoration(
 | |
|                       border: Border(
 | |
|                         bottom: BorderSide(
 | |
|                           color: Theme.of(context).dividerColor,
 | |
|                           width: 1 / devicePixelRatio,
 | |
|                         ),
 | |
|                       ),
 | |
|                     ),
 | |
|                     child: MoveWindow(
 | |
|                       child: Row(
 | |
|                         crossAxisAlignment: CrossAxisAlignment.center,
 | |
|                         mainAxisAlignment:
 | |
|                             Platform.isMacOS
 | |
|                                 ? MainAxisAlignment.center
 | |
|                                 : MainAxisAlignment.start,
 | |
|                         children: [
 | |
|                           Expanded(
 | |
|                             child: Text(
 | |
|                               'Solar Network',
 | |
|                               textAlign:
 | |
|                                   Platform.isMacOS
 | |
|                                       ? TextAlign.center
 | |
|                                       : TextAlign.start,
 | |
|                             ).padding(horizontal: 12, vertical: 5),
 | |
|                           ),
 | |
|                           if (!Platform.isMacOS)
 | |
|                             MinimizeWindowButton(colors: windowButtonColor),
 | |
|                           if (!Platform.isMacOS)
 | |
|                             MaximizeWindowButton(colors: windowButtonColor),
 | |
|                           if (!Platform.isMacOS)
 | |
|                             CloseWindowButton(
 | |
|                               colors: windowButtonColor,
 | |
|                               onPressed: () => appWindow.hide(),
 | |
|                             ),
 | |
|                         ],
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ),
 | |
|                 Expanded(child: child),
 | |
|               ],
 | |
|             ),
 | |
|             _WebSocketIndicator(),
 | |
|           ],
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return Stack(
 | |
|       fit: StackFit.expand,
 | |
|       children: [child, _WebSocketIndicator()],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| final rootScaffoldKey = GlobalKey<ScaffoldState>();
 | |
| 
 | |
| class AppScaffold extends StatelessWidget {
 | |
|   final Widget? body;
 | |
|   final PreferredSizeWidget? bottomNavigationBar;
 | |
|   final PreferredSizeWidget? bottomSheet;
 | |
|   final Drawer? drawer;
 | |
|   final Widget? endDrawer;
 | |
|   final FloatingActionButtonAnimator? floatingActionButtonAnimator;
 | |
|   final FloatingActionButtonLocation? floatingActionButtonLocation;
 | |
|   final Widget? floatingActionButton;
 | |
|   final AppBar? appBar;
 | |
|   final DrawerCallback? onDrawerChanged;
 | |
|   final DrawerCallback? onEndDrawerChanged;
 | |
|   final bool noBackground;
 | |
| 
 | |
|   const AppScaffold({
 | |
|     super.key,
 | |
|     this.appBar,
 | |
|     this.body,
 | |
|     this.floatingActionButton,
 | |
|     this.floatingActionButtonLocation,
 | |
|     this.floatingActionButtonAnimator,
 | |
|     this.bottomNavigationBar,
 | |
|     this.bottomSheet,
 | |
|     this.drawer,
 | |
|     this.endDrawer,
 | |
|     this.onDrawerChanged,
 | |
|     this.onEndDrawerChanged,
 | |
|     this.noBackground = false,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final appBarHeight = appBar?.preferredSize.height ?? 0;
 | |
|     final safeTop = MediaQuery.of(context).padding.top;
 | |
| 
 | |
|     final content = Column(
 | |
|       children: [
 | |
|         IgnorePointer(
 | |
|           child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0),
 | |
|         ),
 | |
|         if (body != null) Expanded(child: body!),
 | |
|       ],
 | |
|     );
 | |
| 
 | |
|     return Scaffold(
 | |
|       extendBody: true,
 | |
|       extendBodyBehindAppBar: true,
 | |
|       backgroundColor:
 | |
|           noBackground
 | |
|               ? Colors.transparent
 | |
|               : Theme.of(context).scaffoldBackgroundColor,
 | |
|       body: SizedBox.expand(
 | |
|         child:
 | |
|             noBackground
 | |
|                 ? content
 | |
|                 : AppBackground(isRoot: true, child: content),
 | |
|       ),
 | |
|       appBar: appBar,
 | |
|       bottomNavigationBar: bottomNavigationBar,
 | |
|       bottomSheet: bottomSheet,
 | |
|       drawer: drawer,
 | |
|       endDrawer: endDrawer,
 | |
|       floatingActionButton: floatingActionButton,
 | |
|       floatingActionButtonAnimator: floatingActionButtonAnimator,
 | |
|       floatingActionButtonLocation: floatingActionButtonLocation,
 | |
|       onDrawerChanged: onDrawerChanged,
 | |
|       onEndDrawerChanged: onEndDrawerChanged,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PageBackButton extends StatelessWidget {
 | |
|   final List<Shadow>? shadows;
 | |
|   const PageBackButton({super.key, this.shadows});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return IconButton(
 | |
|       onPressed: () {
 | |
|         context.router.maybePop();
 | |
|       },
 | |
|       icon: Icon(
 | |
|         (!kIsWeb && (Platform.isMacOS || Platform.isIOS))
 | |
|             ? Symbols.arrow_back_ios_new
 | |
|             : Symbols.arrow_back,
 | |
|         shadows: shadows,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| const kAppBackgroundImagePath = 'island_app_background';
 | |
| 
 | |
| final backgroundImageFileProvider = FutureProvider<File?>((ref) async {
 | |
|   if (kIsWeb) return null;
 | |
|   final dir = await getApplicationSupportDirectory();
 | |
|   final path = '${dir.path}/$kAppBackgroundImagePath';
 | |
|   final file = File(path);
 | |
|   return file.existsSync() ? file : null;
 | |
| });
 | |
| 
 | |
| class AppBackground extends ConsumerWidget {
 | |
|   final Widget child;
 | |
|   final bool isRoot;
 | |
| 
 | |
|   const AppBackground({super.key, required this.child, this.isRoot = false});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final imageFileAsync = ref.watch(backgroundImageFileProvider);
 | |
| 
 | |
|     if (isRoot || ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) {
 | |
|       return imageFileAsync.when(
 | |
|         data: (file) {
 | |
|           if (file != null) {
 | |
|             final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
 | |
| 
 | |
|             final size = MediaQuery.of(context).size;
 | |
|             return Container(
 | |
|               color: Theme.of(context).colorScheme.surface,
 | |
|               child: Container(
 | |
|                 decoration: BoxDecoration(
 | |
|                   backgroundBlendMode: BlendMode.darken,
 | |
|                   color: Theme.of(context).colorScheme.surface,
 | |
|                   image: DecorationImage(
 | |
|                     opacity: 0.2,
 | |
|                     image: ResizeImage(
 | |
|                       FileImage(file),
 | |
|                       width: (size.width * devicePixelRatio).round(),
 | |
|                       height: (size.height * devicePixelRatio).round(),
 | |
|                       policy: ResizeImagePolicy.fit,
 | |
|                     ),
 | |
|                     fit: BoxFit.cover,
 | |
|                   ),
 | |
|                 ),
 | |
|                 child: child,
 | |
|               ),
 | |
|             );
 | |
|           }
 | |
|           return Material(
 | |
|             color: Theme.of(context).colorScheme.surface,
 | |
|             child: child,
 | |
|           );
 | |
|         },
 | |
|         loading: () => const SizedBox(),
 | |
|         error:
 | |
|             (_, __) => Material(
 | |
|               color: Theme.of(context).colorScheme.surface,
 | |
|               child: child,
 | |
|             ),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return Material(color: Colors.transparent, child: child);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _WebSocketIndicator extends HookConsumerWidget {
 | |
|   const _WebSocketIndicator();
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final isDesktop =
 | |
|         !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
 | |
| 
 | |
|     final user = ref.watch(userInfoProvider);
 | |
|     final websocketState = ref.watch(websocketStateProvider);
 | |
|     final indicatorHeight =
 | |
|         MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 60);
 | |
| 
 | |
|     Color indicatorColor;
 | |
|     String indicatorText;
 | |
| 
 | |
|     if (websocketState == WebSocketState.connected()) {
 | |
|       indicatorColor = Colors.green;
 | |
|       indicatorText = 'connectionConnected';
 | |
|     } else if (websocketState == WebSocketState.connecting()) {
 | |
|       indicatorColor = Colors.teal;
 | |
|       indicatorText = 'connectionReconnecting';
 | |
|     } else {
 | |
|       indicatorColor = Colors.orange;
 | |
|       indicatorText = 'connectionDisconnected';
 | |
|     }
 | |
| 
 | |
|     return AnimatedPositioned(
 | |
|       duration: Duration(milliseconds: 1850),
 | |
|       top:
 | |
|           !user.hasValue || websocketState == WebSocketState.connected()
 | |
|               ? -indicatorHeight
 | |
|               : 0,
 | |
|       curve: Curves.fastLinearToSlowEaseIn,
 | |
|       left: 0,
 | |
|       right: 0,
 | |
|       height: indicatorHeight,
 | |
|       child: IgnorePointer(
 | |
|         child: Material(
 | |
|           elevation:
 | |
|               !user.hasValue || websocketState == WebSocketState.connected()
 | |
|                   ? 0
 | |
|                   : 4,
 | |
|           child: AnimatedContainer(
 | |
|             duration: Duration(milliseconds: 300),
 | |
|             color: indicatorColor,
 | |
|             child: Center(
 | |
|               child:
 | |
|                   Text(
 | |
|                     indicatorText,
 | |
|                     style: TextStyle(color: Colors.white, fontSize: 16),
 | |
|                   ).tr(),
 | |
|             ).padding(top: MediaQuery.of(context).padding.top),
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |