Files
2026-05-18 20:33:54 +02:00

283 lines
8.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:relationship_saver/app/navigation/app_destination.dart';
import 'package:relationship_saver/core/config/app_theme.dart';
import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_review_view.dart';
import 'package:relationship_saver/features/people/presentation/people_view.dart';
import 'package:relationship_saver/features/settings/presentation/settings_view.dart';
import 'package:relationship_saver/features/share_intake/presentation/share_inbox_view.dart';
/// Responsive shell that hosts the main feature slices.
class AppShell extends ConsumerStatefulWidget {
const AppShell({super.key});
@override
ConsumerState<AppShell> createState() => _AppShellState();
}
class _AppShellState extends ConsumerState<AppShell> {
AppDestination _destination = AppDestination.shareInbox;
static const List<AppDestination> _destinations = AppDestination.values;
static const List<AppDestination> _compactPrimaryDestinations =
AppDestination.values;
@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) {
return _buildCompactLayout(context);
}
return Row(
children: <Widget>[
_Sidebar(
selected: _destination,
destinations: _destinations,
onChange: _selectDestination,
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
layoutBuilder: _expandedSwitcherLayout,
child: KeyedSubtree(
key: ValueKey<AppDestination>(_destination),
child: _screenFor(_destination),
),
),
),
],
);
},
),
),
),
);
}
Widget _buildCompactLayout(BuildContext context) {
final int compactIndex = _compactSelectedIndex(_destination);
final AppDestination active = _compactPrimaryDestinations[compactIndex];
return _CompactShell(
selected: active,
body: _screenFor(active),
destinations: _compactPrimaryDestinations,
onChange: _selectDestination,
);
}
void _selectDestination(AppDestination destination) {
setState(() {
_destination = destination;
});
}
int _compactSelectedIndex(AppDestination destination) {
for (int i = 0; i < _compactPrimaryDestinations.length; i += 1) {
if (_compactPrimaryDestinations[i] == destination) {
return i;
}
}
return 0;
}
Widget _screenFor(AppDestination destination) {
switch (destination) {
case AppDestination.shareInbox:
return const ShareInboxView();
case AppDestination.people:
return const PeopleView();
case AppDestination.aiReview:
return const AiDigestReviewView();
case AppDestination.settings:
return const SettingsView();
}
}
}
class _Sidebar extends StatelessWidget {
const _Sidebar({
required this.selected,
required this.destinations,
required this.onChange,
});
final AppDestination selected;
final List<AppDestination> destinations;
final ValueChanged<AppDestination> 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),
Expanded(
child: Text(
'Relationship Saver',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 22),
...destinations.map((AppDestination destination) {
final bool isSelected = destination == selected;
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: InkWell(
onTap: () => onChange(destination),
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFEAF7FB)
: Colors.transparent,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: <Widget>[
Icon(
destination.icon,
size: 20,
color: isSelected
? AppTheme.primary
: AppTheme.textSecondary,
),
const SizedBox(width: 10),
Text(
destination.label,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(
color: isSelected
? AppTheme.primary
: AppTheme.textSecondary,
),
),
],
),
),
),
);
}),
const Spacer(),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF2F8FB),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Share messages or paste text into Capture. Nothing is assigned until you choose a person.',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary),
),
),
],
),
);
}
}
class _CompactShell extends StatelessWidget {
const _CompactShell({
required this.selected,
required this.body,
required this.destinations,
required this.onChange,
});
final AppDestination selected;
final Widget body;
final List<AppDestination> destinations;
final ValueChanged<AppDestination> onChange;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 220),
layoutBuilder: _expandedSwitcherLayout,
child: KeyedSubtree(
key: ValueKey<AppDestination>(selected),
child: body,
),
),
),
NavigationBar(
selectedIndex: destinations.indexOf(selected),
onDestinationSelected: (int index) => onChange(destinations[index]),
destinations: destinations
.map(
(AppDestination destination) => NavigationDestination(
icon: Icon(destination.icon),
label: destination.label,
),
)
.toList(growable: false),
),
],
);
}
}
Widget _expandedSwitcherLayout(
Widget? currentChild,
List<Widget> previousChildren,
) {
return Stack(
fit: StackFit.expand,
children: <Widget>[...previousChildren, ?currentChild],
);
}