import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; import '../../providers/settings_controller.dart'; import '../../theme/design_tokens.dart'; import '../noo/core/noo_button.dart'; import '../noo/nav/noo_bottom_bar.dart'; import '../noo/noo_layout.dart'; /// Shared loading/error/empty slivers for Recent/Activity/Trash/Shares (and /// any future tab shaped like them: a `CustomScrollView` over one flat /// list), restyled with noo tokens instead of each screen hand-rolling the /// same centered column - see `FilesView`'s identical error/empty layout, /// which this mirrors. /// Centered spinner for a tab's first load (list still empty). const Widget tabLoadingSliver = SliverFillRemaining( hasScrollBody: false, child: Center(child: CircularProgressIndicator()), ); /// Centered error state: icon, title, message, and a Retry button. Widget tabErrorSliver( BuildContext context, { required String title, required String message, required VoidCallback onRetry, }) { final colors = context.nooColors; return SliverFillRemaining( hasScrollBody: false, child: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.circleAlert, size: 56, color: colors.danger), const SizedBox(height: 16), Text( title, textAlign: TextAlign.center, style: NooText.cardTitle.copyWith(color: colors.danger), ), const SizedBox(height: 8), Text( message, textAlign: TextAlign.center, style: NooText.body.copyWith(color: colors.fg3), ), const SizedBox(height: 20), NooButton( variant: NooButtonVariant.secondary, size: NooButtonSize.field, icon: LucideIcons.refreshCw, onTap: onRetry, child: const Text('Retry'), ), ], ), ), ), ); } /// Centered empty state: an icon over a message. Widget tabEmptySliver( BuildContext context, { required IconData icon, required String message, }) { final colors = context.nooColors; return SliverFillRemaining( hasScrollBody: false, child: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 56, color: colors.fg3), const SizedBox(height: 12), Text( message, textAlign: TextAlign.center, style: NooText.cardTitle.copyWith(color: colors.fg2), ), ], ), ), ), ); } /// How much bottom padding a tab's scrollable list needs to clear /// [NooBottomBar] and not just butt up against it. [NooBottomBarStyle. /// attached] bars aren't drawn behind (no `Scaffold.extendBody`), so /// Scaffold already shrinks the body above them - this is then pure /// breathing room, not overlap prevention. [NooBottomBarStyle.floating] /// bars draw over an extended body instead, so nothing reserves space for /// them automatically: the clearance has to cover the bar's own footprint /// (see [NooBottomBar.rowHeight]/[NooBottomBar.floatingBottomMargin]) plus /// the safe-area inset below it, or the last row ends up hidden under it. double bottomBarClearance(BuildContext context) { final settings = context.watch(); if (settings.bottomBarStyle == NooBottomBarStyle.attached) return 100; final barHeight = NooBottomBar.rowHeight( NooLayout.navStyle(context), NooBottomBarStyle.floating, ); final safeBottom = MediaQuery.paddingOf(context).bottom; return barHeight + NooBottomBar.floatingBottomMargin + safeBottom + 24; } /// Bottom padding sliver so the last row/card clears the bottom nav/FAB - /// same trailing pair every rebuilt tab list ends on. List tabBottomInsetSlivers(BuildContext context) => [ SliverToBoxAdapter(child: SizedBox(height: bottomBarClearance(context))), const SliverFillRemaining(hasScrollBody: false, child: SizedBox()), ];