feat: adopt smolmail protocol 1.1, adaptive nav shell, and server mail retention

This commit is contained in:
randogoth 2026-09-27 11:07:11 +03:00
parent eaaa3f2ede
commit c693e6fcb9
27 changed files with 1190 additions and 592 deletions

View file

@ -0,0 +1,86 @@
import "package:auto_route/auto_route.dart";
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:smol_mail/data/providers/providers.dart";
import "package:smol_mail/presentation/routes/app_router.gr.dart";
import "package:smol_mail/presentation/theme/app_colors.dart";
import "package:smol_mail/presentation/theme/breakpoints.dart";
import "package:smol_mail/shared/configs/flash_mail_icons.dart";
class _Destination {
final Widget icon;
final String label;
const _Destination({required this.icon, required this.label});
}
/// The app's only persistent navigation surface: a bottom [NavigationBar] on
/// phone-width screens, a side [NavigationRail] on desktop-width ones — no
/// hamburger drawer at either width. Mail/Contacts/Settings are switched by
/// tapping only, never by swipe, so a horizontal gesture inside a
/// destination (e.g. the Inbox/Sent/Requests tab bar) never changes section.
@RoutePage()
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final unread = ref.watch(unreadProvider) + ref.watch(requestsUnreadProvider);
final appColors = Theme.of(context).extension<AppColors>()!;
final mailIcon = Icon(FlashMailIcons.inbox);
final destinations = [
_Destination(
icon: unread > 0 ? Badge(label: Text("$unread"), child: mailIcon) : mailIcon,
label: "Mail",
),
const _Destination(icon: Icon(Icons.contacts), label: "Contacts"),
const _Destination(icon: Icon(Icons.settings), label: "Settings"),
];
return AutoTabsRouter.builder(
routes: const [InboxRoute(), ContactsRoute(), SettingsRoute()],
builder: (context, children, tabsRouter) {
final body = IndexedStack(index: tabsRouter.activeIndex, children: children);
final wide = MediaQuery.sizeOf(context).width >= kDesktopBreakpoint;
if (!wide) {
return Scaffold(
body: body,
bottomNavigationBar: NavigationBar(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: tabsRouter.setActiveIndex,
indicatorColor: appColors.highlight,
destinations: [
for (final d in destinations)
NavigationDestination(icon: d.icon, label: d.label),
],
),
);
}
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: tabsRouter.setActiveIndex,
backgroundColor: appColors.cardFill,
useIndicator: true,
indicatorColor: appColors.highlight,
labelType: NavigationRailLabelType.all,
destinations: [
for (final d in destinations)
NavigationRailDestination(icon: d.icon, label: Text(d.label)),
],
),
const VerticalDivider(width: 1),
Expanded(child: body),
],
),
);
},
);
}
}