Files
2026-07-14 11:11:36 +08:00

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