282 lines
8.2 KiB
Dart
282 lines
8.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../about/internal/application/TextType.dart';
|
|
|
|
import '../../utils/Colors.dart';
|
|
import '../text/Text.dart';
|
|
|
|
class NavItemData {
|
|
final IconData icon;
|
|
final String label;
|
|
final int? badge;
|
|
final VoidCallback onTap;
|
|
final bool selected;
|
|
const NavItemData({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.onTap,
|
|
this.badge,
|
|
this.selected = false,
|
|
});
|
|
}
|
|
|
|
/// Left navigation rail / sidebar. [expanded] shows labels; otherwise a
|
|
/// compact icon rail.
|
|
class SideNav extends StatelessWidget {
|
|
final bool expanded;
|
|
final List<NavItemData> items;
|
|
final ThemeMode themeMode;
|
|
final Future<void> Function() onCycleTheme;
|
|
final VoidCallback onNewServer;
|
|
|
|
const SideNav({
|
|
super.key,
|
|
required this.expanded,
|
|
required this.items,
|
|
required this.themeMode,
|
|
required this.onCycleTheme,
|
|
required this.onNewServer,
|
|
});
|
|
|
|
(IconData, String) get _themeIcon => switch (themeMode) {
|
|
ThemeMode.dark => (Icons.dark_mode, 'Dark'),
|
|
ThemeMode.light => (Icons.light_mode, 'Light'),
|
|
ThemeMode.system => (Icons.brightness_auto, 'System'),
|
|
};
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final jp = context.jp;
|
|
final (themeI, themeLabel) = _themeIcon;
|
|
return Container(
|
|
width: expanded ? 236 : 78,
|
|
decoration: BoxDecoration(
|
|
color: jp.surface,
|
|
border: Border(right: BorderSide(color: jp.border)),
|
|
),
|
|
child: SafeArea(
|
|
right: false,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(expanded ? 20 : 0, 20, 0, 20),
|
|
child: Row(
|
|
mainAxisAlignment: expanded
|
|
? MainAxisAlignment.start
|
|
: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
gradient: PeachTheme.brandGradient,
|
|
borderRadius: BorderRadius.circular(11),
|
|
boxShadow: PeachTheme.glow(
|
|
PeachTheme.seed,
|
|
opacity: 0.5,
|
|
blur: 18,
|
|
),
|
|
),
|
|
child: const Icon(
|
|
Icons.rocket_launch,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
),
|
|
if (expanded) ...[
|
|
const SizedBox(width: 12),
|
|
text(
|
|
'Peach',
|
|
20,
|
|
TextType.Bold,
|
|
color: jp.text,
|
|
weight: FontWeight.w800,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView(
|
|
padding: EdgeInsets.symmetric(horizontal: expanded ? 12 : 10),
|
|
children: [for (final it in items) _navItem(context, it)],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: expanded ? 12 : 10),
|
|
child: Column(
|
|
children: [
|
|
_footerButton(
|
|
context,
|
|
themeI,
|
|
themeLabel,
|
|
(_) => onCycleTheme(),
|
|
),
|
|
const SizedBox(height: 6),
|
|
_newServerButton(context),
|
|
const SizedBox(height: 14),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _navItem(BuildContext context, NavItemData it) {
|
|
final jp = context.jp;
|
|
final inner = Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: expanded ? 14 : 0,
|
|
vertical: 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: it.selected
|
|
? PeachTheme.seed.withValues(alpha: 0.13)
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: expanded
|
|
? MainAxisAlignment.start
|
|
: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
it.icon,
|
|
size: 20,
|
|
color: it.selected ? PeachTheme.seed : jp.textDim,
|
|
),
|
|
if (expanded) ...[
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: text(
|
|
it.label,
|
|
14,
|
|
TextType.Regular,
|
|
color: it.selected ? jp.text : jp.textDim,
|
|
overflow: TextOverflow.ellipsis,
|
|
weight: it.selected ? FontWeight.w700 : FontWeight.w600,
|
|
),
|
|
),
|
|
if (it.badge != null && it.badge! > 0)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
|
|
decoration: BoxDecoration(
|
|
color: jp.surface2,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: text(
|
|
'${it.badge}',
|
|
11,
|
|
TextType.Regular,
|
|
color: jp.textDim,
|
|
weight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
// A premium left accent bar marks the selected item.
|
|
final content = Container(
|
|
margin: const EdgeInsets.only(bottom: 6),
|
|
child: Row(
|
|
children: [
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 160),
|
|
width: 3,
|
|
height: it.selected ? 22 : 0,
|
|
margin: EdgeInsets.only(right: expanded ? 8 : 0),
|
|
decoration: BoxDecoration(
|
|
color: PeachTheme.seed,
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
),
|
|
Expanded(child: inner),
|
|
],
|
|
),
|
|
);
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: it.onTap,
|
|
child: expanded ? content : Tooltip(message: it.label, child: content),
|
|
);
|
|
}
|
|
|
|
Widget _footerButton(
|
|
BuildContext context,
|
|
IconData icon,
|
|
String label,
|
|
void Function(BuildContext) onTap,
|
|
) {
|
|
final jp = context.jp;
|
|
final w = InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: () => onTap(context),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: expanded ? 14 : 0,
|
|
vertical: 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: jp.border),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: expanded
|
|
? MainAxisAlignment.start
|
|
: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 19, color: jp.textDim),
|
|
if (expanded) ...[
|
|
const SizedBox(width: 12),
|
|
text(
|
|
'Theme: $label',
|
|
13,
|
|
TextType.Regular,
|
|
color: jp.textDim,
|
|
weight: FontWeight.w400,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
return expanded ? w : Tooltip(message: 'Theme: $label', child: w);
|
|
}
|
|
|
|
Widget _newServerButton(BuildContext context) {
|
|
if (!expanded) {
|
|
return Tooltip(
|
|
message: 'New server',
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(12),
|
|
onTap: onNewServer,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
gradient: PeachTheme.brandGradient,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: PeachTheme.glow(
|
|
PeachTheme.seed,
|
|
opacity: 0.5,
|
|
blur: 16,
|
|
),
|
|
),
|
|
child: const Icon(Icons.add, color: Colors.white),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton.icon(
|
|
onPressed: onNewServer,
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('New server'),
|
|
),
|
|
);
|
|
}
|
|
}
|