Files
rely/lib/features/home/app_shell.dart
T
2026-02-15 15:20:23 +01:00

389 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:relationship_saver/core/config/app_theme.dart';
import 'package:relationship_saver/features/dashboard/dashboard_view.dart';
import 'package:relationship_saver/features/ideas/ideas_view.dart';
import 'package:relationship_saver/features/moments/moments_view.dart';
import 'package:relationship_saver/features/people/people_view.dart';
import 'package:relationship_saver/features/reminders/reminders_view.dart';
import 'package:relationship_saver/features/settings/settings_view.dart';
import 'package:relationship_saver/features/signals/signals_view.dart';
import 'package:relationship_saver/features/sync/sync_view.dart';
class AppShell extends ConsumerStatefulWidget {
const AppShell({super.key});
@override
ConsumerState<AppShell> createState() => _AppShellState();
}
class _AppShellState extends ConsumerState<AppShell> {
int _index = 0;
static const List<_ShellDestination> _destinations = <_ShellDestination>[
_ShellDestination('Dashboard', Icons.dashboard_rounded, 0),
_ShellDestination('People', Icons.people_alt_rounded, 1),
_ShellDestination('Moments', Icons.auto_awesome_rounded, 2),
_ShellDestination('Signals', Icons.tips_and_updates_rounded, 3),
_ShellDestination('Ideas', Icons.lightbulb_outline_rounded, 4),
_ShellDestination('Reminders', Icons.notifications_active_outlined, 5),
_ShellDestination('Sync', Icons.sync_rounded, 6),
_ShellDestination('Settings', Icons.settings_rounded, 7),
];
static const List<_ShellDestination> _compactPrimaryDestinations =
<_ShellDestination>[
_ShellDestination('Dashboard', Icons.dashboard_rounded, 0),
_ShellDestination('People', Icons.people_alt_rounded, 1),
_ShellDestination('Moments', Icons.auto_awesome_rounded, 2),
_ShellDestination('Signals', Icons.tips_and_updates_rounded, 3),
];
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
Color(0xFFF3F8FB),
Color(0xFFEAF1F8),
Color(0xFFF8FAFC),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth < 1000) {
final int compactIndex = _compactSelectedIndex(_index);
final _ShellDestination active =
_compactPrimaryDestinations[compactIndex];
return _CompactShell(
title: active.label,
index: compactIndex,
body: _screenFor(active.screenIndex),
destinations: _compactPrimaryDestinations,
onChange: (int value) {
setState(() {
_index = _compactPrimaryDestinations[value].screenIndex;
});
},
onOpenSecondary: (_SecondaryDestination destination) {
final int screenIndex = switch (destination) {
_SecondaryDestination.ideas => 4,
_SecondaryDestination.reminders => 5,
_SecondaryDestination.sync => 6,
_SecondaryDestination.settings => 7,
};
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return _CompactSecondaryScreen(
title: _destinations[screenIndex].label,
child: _screenFor(screenIndex),
);
},
),
);
},
);
}
return Row(
children: <Widget>[
_Sidebar(
index: _index,
destinations: _destinations,
onChange: (int value) {
setState(() {
_index = value;
});
},
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: KeyedSubtree(
key: ValueKey<int>(_index),
child: _screenFor(_index),
),
),
),
],
);
},
),
),
),
);
}
int _compactSelectedIndex(int fullIndex) {
for (int i = 0; i < _compactPrimaryDestinations.length; i += 1) {
if (_compactPrimaryDestinations[i].screenIndex == fullIndex) {
return i;
}
}
return 0;
}
Widget _screenFor(int index) {
switch (index) {
case 0:
return const DashboardView();
case 1:
return const PeopleView();
case 2:
return const MomentsView();
case 3:
return const SignalsView();
case 4:
return const IdeasView();
case 5:
return const RemindersView();
case 6:
return const SyncView();
case 7:
return const SettingsView();
default:
return const DashboardView();
}
}
}
class _Sidebar extends StatelessWidget {
const _Sidebar({
required this.index,
required this.destinations,
required this.onChange,
});
final int index;
final List<_ShellDestination> destinations;
final ValueChanged<int> onChange;
@override
Widget build(BuildContext context) {
return Container(
width: 290,
margin: const EdgeInsets.fromLTRB(20, 20, 8, 20),
padding: const EdgeInsets.fromLTRB(18, 20, 18, 18),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.78),
borderRadius: BorderRadius.circular(28),
border: Border.all(color: Colors.white.withValues(alpha: 0.7)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
gradient: const LinearGradient(
colors: <Color>[Color(0xFF1AB6C8), Color(0xFF286DE0)],
),
),
child: const Icon(
Icons.favorite_rounded,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 10),
Text(
'Relationship Saver',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 22),
...destinations.indexed.map(((int, _ShellDestination) tuple) {
final int destinationIndex = tuple.$1;
final _ShellDestination destination = tuple.$2;
final bool selected = destinationIndex == index;
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: InkWell(
onTap: () => onChange(destinationIndex),
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
color: selected
? const Color(0xFFEAF7FB)
: Colors.transparent,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: <Widget>[
Icon(
destination.icon,
size: 20,
color: selected
? AppTheme.primary
: AppTheme.textSecondary,
),
const SizedBox(width: 10),
Text(
destination.label,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(
color: selected
? AppTheme.primary
: AppTheme.textSecondary,
),
),
],
),
),
),
);
}),
const Spacer(),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF2F8FB),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Tip: Use Signals daily and convert at least one into a concrete plan.',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary),
),
),
],
),
);
}
}
class _CompactShell extends StatelessWidget {
const _CompactShell({
required this.title,
required this.index,
required this.body,
required this.destinations,
required this.onChange,
required this.onOpenSecondary,
});
final String title;
final int index;
final Widget body;
final List<_ShellDestination> destinations;
final ValueChanged<int> onChange;
final ValueChanged<_SecondaryDestination> onOpenSecondary;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Row(
children: <Widget>[
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
),
PopupMenuButton<_SecondaryDestination>(
tooltip: 'More',
icon: const Icon(Icons.more_horiz_rounded),
onSelected: onOpenSecondary,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<_SecondaryDestination>>[
const PopupMenuItem<_SecondaryDestination>(
value: _SecondaryDestination.ideas,
child: Text('Ideas'),
),
const PopupMenuItem<_SecondaryDestination>(
value: _SecondaryDestination.reminders,
child: Text('Reminders'),
),
const PopupMenuItem<_SecondaryDestination>(
value: _SecondaryDestination.sync,
child: Text('Sync'),
),
const PopupMenuItem<_SecondaryDestination>(
value: _SecondaryDestination.settings,
child: Text('Settings'),
),
],
),
],
),
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 220),
child: KeyedSubtree(key: ValueKey<int>(index), child: body),
),
),
NavigationBar(
selectedIndex: index,
onDestinationSelected: onChange,
destinations: destinations
.map(
(_ShellDestination destination) => NavigationDestination(
icon: Icon(destination.icon),
label: destination.label,
),
)
.toList(growable: false),
),
],
);
}
}
class _CompactSecondaryScreen extends StatelessWidget {
const _CompactSecondaryScreen({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Color(0xFFF3F8FB), Color(0xFFEAF1F8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(title: Text(title)),
body: child,
),
);
}
}
enum _SecondaryDestination { ideas, reminders, sync, settings }
class _ShellDestination {
const _ShellDestination(this.label, this.icon, this.screenIndex);
final String label;
final IconData icon;
final int screenIndex;
}