Surface/lib/widgets/app_scaffold.dart

287 lines
8.9 KiB
Dart

import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.dart';
import 'package:island/route.gr.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:path_provider/path_provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:styled_widget/styled_widget.dart';
class WindowScaffold extends StatelessWidget {
final Widget child;
final AppRouter router;
const WindowScaffold({super.key, required this.child, required this.router});
@override
Widget build(BuildContext context) {
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: 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),
],
),
);
}
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent,
body: SizedBox.expand(child: child),
key: rootScaffoldKey,
bottomNavigationBar:
router.current.meta['bottomNav'] == true || router.currentPath == '/'
? AppBottomNavigationBar(router: router)
: null,
);
}
}
class AppBottomNavigationBar extends HookConsumerWidget {
const AppBottomNavigationBar({super.key, required this.router});
final AppRouter router;
@override
Widget build(BuildContext context, WidgetRef ref) {
final destination = useState(0);
return NavigationBar(
selectedIndex: destination.value,
destinations: [
NavigationDestination(
icon: Icon(LucideIcons.compass),
label: 'Explore',
),
NavigationDestination(
icon: Icon(LucideIcons.userCircle),
label: 'Account',
),
],
onDestinationSelected: (idx) {
switch (idx) {
case 0:
destination.value = idx;
router.replace(ExploreRoute());
break;
case 1:
destination.value = idx;
router.replace(AccountRoute());
break;
}
},
);
}
}
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 {
const PageBackButton({super.key});
@override
Widget build(BuildContext context) {
return BackButton(
onPressed: () {
context.router.maybePop();
},
);
}
}
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);
}
}