188 lines
5.6 KiB
Dart
188 lines
5.6 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter/services.dart';
|
||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
|
import 'package:go_router/go_router.dart';
|
||
|
|
|
||
|
|
import '../../providers/session_provider.dart';
|
||
|
|
import '../../providers/shell_backdrop_provider.dart';
|
||
|
|
import '../constants/breakpoints.dart';
|
||
|
|
import '../theme/app_theme.dart';
|
||
|
|
import 'app_bottom_nav.dart';
|
||
|
|
import 'shell_backdrop_layer.dart';
|
||
|
|
import 'sidebar.dart';
|
||
|
|
import 'window_chrome.dart';
|
||
|
|
import 'window_chrome_overlay.dart';
|
||
|
|
|
||
|
|
class AppShell extends ConsumerStatefulWidget {
|
||
|
|
final StatefulNavigationShell navigationShell;
|
||
|
|
|
||
|
|
|
||
|
|
final String location;
|
||
|
|
|
||
|
|
const AppShell({
|
||
|
|
super.key,
|
||
|
|
required this.navigationShell,
|
||
|
|
required this.location,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
ConsumerState<AppShell> createState() => _AppShellState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _AppShellState extends ConsumerState<AppShell> {
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
initSidebarState(ref);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final immersive =
|
||
|
|
widget.location == '/' ||
|
||
|
|
widget.location == '/discover' ||
|
||
|
|
widget.location.startsWith('/library/');
|
||
|
|
|
||
|
|
final sessionAsync = ref.watch(sessionProvider);
|
||
|
|
final unauthenticatedHome =
|
||
|
|
widget.location == '/' &&
|
||
|
|
sessionAsync.hasValue &&
|
||
|
|
ref.watch(activeSessionProvider) == null;
|
||
|
|
final effectiveImmersive = immersive && !unauthenticatedHome;
|
||
|
|
|
||
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
|
|
if (!mounted) return;
|
||
|
|
ref.read(shellLocationProvider.notifier).set(widget.location);
|
||
|
|
});
|
||
|
|
|
||
|
|
final themeBrightness = Theme.of(context).brightness;
|
||
|
|
final overlayStyle = effectiveImmersive
|
||
|
|
? const SystemUiOverlayStyle(
|
||
|
|
statusBarColor: Colors.transparent,
|
||
|
|
statusBarIconBrightness: Brightness.light,
|
||
|
|
statusBarBrightness: Brightness.dark,
|
||
|
|
systemNavigationBarColor: Colors.transparent,
|
||
|
|
systemNavigationBarIconBrightness: Brightness.light,
|
||
|
|
)
|
||
|
|
: SystemUiOverlayStyle(
|
||
|
|
statusBarColor: Colors.transparent,
|
||
|
|
statusBarIconBrightness: themeBrightness == Brightness.dark
|
||
|
|
? Brightness.light
|
||
|
|
: Brightness.dark,
|
||
|
|
statusBarBrightness: themeBrightness,
|
||
|
|
systemNavigationBarColor: Colors.transparent,
|
||
|
|
systemNavigationBarIconBrightness:
|
||
|
|
themeBrightness == Brightness.dark
|
||
|
|
? Brightness.light
|
||
|
|
: Brightness.dark,
|
||
|
|
);
|
||
|
|
|
||
|
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||
|
|
value: overlayStyle,
|
||
|
|
child: Scaffold(
|
||
|
|
backgroundColor: Colors.transparent,
|
||
|
|
body: LayoutBuilder(
|
||
|
|
builder: (context, constraints) {
|
||
|
|
final isCompact = Breakpoints.isCompact(constraints.maxWidth);
|
||
|
|
return Stack(
|
||
|
|
children: [
|
||
|
|
Positioned.fill(
|
||
|
|
child: ShellBackdropLayer(active: effectiveImmersive),
|
||
|
|
),
|
||
|
|
if (isCompact)
|
||
|
|
_CompactBody(navigationShell: widget.navigationShell)
|
||
|
|
else
|
||
|
|
Row(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
|
|
children: [
|
||
|
|
Sidebar(
|
||
|
|
selectedBranchIndex:
|
||
|
|
widget.navigationShell.currentIndex,
|
||
|
|
onNavigateBranch: (index, {initialLocation = false}) =>
|
||
|
|
widget.navigationShell.goBranch(
|
||
|
|
index,
|
||
|
|
initialLocation: initialLocation,
|
||
|
|
),
|
||
|
|
location: widget.location,
|
||
|
|
immersiveRoute: effectiveImmersive,
|
||
|
|
),
|
||
|
|
Expanded(
|
||
|
|
child: RepaintBoundary(child: widget.navigationShell),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
_AppTitleBarOverlay(immersive: effectiveImmersive),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class _CompactBody extends StatelessWidget {
|
||
|
|
final StatefulNavigationShell navigationShell;
|
||
|
|
|
||
|
|
const _CompactBody({required this.navigationShell});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return Scaffold(
|
||
|
|
backgroundColor: Colors.transparent,
|
||
|
|
extendBody: true,
|
||
|
|
body: RepaintBoundary(child: navigationShell),
|
||
|
|
bottomNavigationBar: AppBottomNav(navigationShell: navigationShell),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class _AppTitleBarOverlay extends ConsumerWidget {
|
||
|
|
|
||
|
|
final bool immersive;
|
||
|
|
|
||
|
|
const _AppTitleBarOverlay({required this.immersive});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
||
|
|
if (!WindowChrome.isDesktop) {
|
||
|
|
return const SizedBox.shrink();
|
||
|
|
}
|
||
|
|
final tokens = context.appTokens;
|
||
|
|
|
||
|
|
final immersiveBackdrop =
|
||
|
|
immersive && ref.watch(shellBackdropProvider) != null;
|
||
|
|
|
||
|
|
if (!immersiveBackdrop) {
|
||
|
|
return const WindowChromeOverlay();
|
||
|
|
}
|
||
|
|
|
||
|
|
return Positioned(
|
||
|
|
top: 0,
|
||
|
|
left: 0,
|
||
|
|
right: 0,
|
||
|
|
height: tokens.titleBarHeight,
|
||
|
|
child: const Stack(
|
||
|
|
children: [
|
||
|
|
Positioned.fill(
|
||
|
|
child: IgnorePointer(
|
||
|
|
child: DecoratedBox(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
gradient: LinearGradient(
|
||
|
|
begin: Alignment.topCenter,
|
||
|
|
end: Alignment.bottomCenter,
|
||
|
|
colors: [Color(0x2E000000), Color(0x00000000)],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
WindowChromeOverlay(onDarkSurface: true),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|