You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meshcore-client/lib/widgets/app_shell.dart

154 lines
4.5 KiB

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/ui_view_state_service.dart';
import 'quick_switch_bar.dart';
/// Shared shell for the primary views (Contacts / Channels / Map).
///
/// Owns the bottom [QuickSwitchBar] that each view previously mounted itself,
/// plus the left nav drawer: a transient slide-out on narrow layouts, and a
/// dockable pane that can be pinned open on wide ones.
///
/// The bottom bar stays a bottom bar at every width; it never becomes a rail.
class AppShell extends StatelessWidget {
static const double wideBreakpoint = 720;
static const double _drawerWidth = 300;
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final int contactsUnreadCount;
final int channelsUnreadCount;
final PreferredSizeWidget? appBar;
final Widget body;
final Widget? floatingActionButton;
/// List content for the active view. Null renders an empty drawer body.
final Widget? drawerContent;
const AppShell({
super.key,
required this.selectedIndex,
required this.onDestinationSelected,
required this.body,
this.appBar,
this.floatingActionButton,
this.drawerContent,
this.contactsUnreadCount = 0,
this.channelsUnreadCount = 0,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= wideBreakpoint;
final pinned =
isWide && context.watch<UiViewStateService>().navDrawerPinned;
return pinned
? _buildPinned(context)
: _buildTransient(context, isWide);
},
);
}
Widget _bottomBar() {
return SafeArea(
top: false,
child: QuickSwitchBar(
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
contactsUnreadCount: contactsUnreadCount,
channelsUnreadCount: channelsUnreadCount,
),
);
}
/// Wide + pinned: the panel is laid out beside the body, not overlaid.
Widget _buildPinned(BuildContext context) {
return Scaffold(
appBar: appBar,
body: SafeArea(
top: false,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: _drawerWidth,
child: _NavPanel(isWide: true, content: drawerContent),
),
const VerticalDivider(width: 1),
Expanded(child: body),
],
),
),
floatingActionButton: floatingActionButton,
bottomNavigationBar: _bottomBar(),
);
}
/// Narrow, or wide-but-unpinned: standard transient drawer. Scaffold injects
/// the hamburger into the app bar automatically.
Widget _buildTransient(BuildContext context, bool isWide) {
return Scaffold(
appBar: appBar,
drawer: Drawer(
width: _drawerWidth,
child: _NavPanel(isWide: isWide, content: drawerContent),
),
body: body,
floatingActionButton: floatingActionButton,
bottomNavigationBar: _bottomBar(),
);
}
}
/// Drawer contents. The pin control appears only on wide layouts, where
/// docking the panel open is useful.
class _NavPanel extends StatelessWidget {
final bool isWide;
final Widget? content;
const _NavPanel({required this.isWide, this.content});
@override
Widget build(BuildContext context) {
final uiState = context.watch<UiViewStateService>();
final pinned = uiState.navDrawerPinned;
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isWide)
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
child: Row(
children: [
Expanded(
child: Text(
MaterialLocalizations.of(context).drawerLabel,
style: Theme.of(context).textTheme.titleSmall,
),
),
IconButton(
icon: Icon(
pinned ? Icons.push_pin : Icons.push_pin_outlined,
),
isSelected: pinned,
tooltip: pinned ? 'Unpin panel' : 'Pin panel open',
onPressed: () => uiState.setNavDrawerPinned(!pinned),
),
],
),
),
if (isWide) const Divider(height: 1),
Expanded(child: content ?? const SizedBox.shrink()),
],
),
);
}
}

Powered by TurnKey Linux.