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),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|