256 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			256 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:math' as math;
 | 
						|
import 'dart:ui';
 | 
						|
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:go_router/go_router.dart';
 | 
						|
import 'package:island/screens/notification.dart';
 | 
						|
import 'package:island/services/responsive.dart';
 | 
						|
import 'package:island/widgets/navigation/conditional_bottom_nav.dart';
 | 
						|
import 'package:island/widgets/navigation/fab_menu.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
import 'package:island/pods/config.dart';
 | 
						|
 | 
						|
final currentRouteProvider = StateProvider<String?>((ref) => null);
 | 
						|
 | 
						|
const kWideScreenRouteStart = 4;
 | 
						|
const kTabRoutes = [
 | 
						|
  '/',
 | 
						|
  '/chat',
 | 
						|
  '/realms',
 | 
						|
  '/account',
 | 
						|
  '/creators',
 | 
						|
  '/developers',
 | 
						|
];
 | 
						|
 | 
						|
class TabsScreen extends HookConsumerWidget {
 | 
						|
  final Widget? child;
 | 
						|
  const TabsScreen({super.key, this.child});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    // final useHorizontalLayout = isWideScreen(context);
 | 
						|
    final currentLocation = GoRouterState.of(context).uri.toString();
 | 
						|
 | 
						|
    // Update the current route provider whenever the location changes
 | 
						|
    useEffect(() {
 | 
						|
      WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
						|
        ref.read(currentRouteProvider.notifier).state = currentLocation;
 | 
						|
      });
 | 
						|
      return null;
 | 
						|
    }, [currentLocation]);
 | 
						|
 | 
						|
    final notificationUnreadCount = ref.watch(
 | 
						|
      notificationUnreadCountNotifierProvider,
 | 
						|
    );
 | 
						|
 | 
						|
    final wideScreen = isWideScreen(context);
 | 
						|
 | 
						|
    final destinations = [
 | 
						|
      NavigationDestination(
 | 
						|
        label: 'explore'.tr(),
 | 
						|
        icon: const Icon(Symbols.explore),
 | 
						|
      ),
 | 
						|
      NavigationDestination(
 | 
						|
        label: 'chat'.tr(),
 | 
						|
        icon: const Icon(Symbols.chat_rounded),
 | 
						|
      ),
 | 
						|
      NavigationDestination(
 | 
						|
        label: 'realms'.tr(),
 | 
						|
        icon: const Icon(Symbols.group),
 | 
						|
      ),
 | 
						|
      NavigationDestination(
 | 
						|
        label: 'account'.tr(),
 | 
						|
        icon: Badge.count(
 | 
						|
          count: notificationUnreadCount.value ?? 0,
 | 
						|
          isLabelVisible: (notificationUnreadCount.value ?? 0) > 0,
 | 
						|
          child: const Icon(Symbols.account_circle),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      if (wideScreen)
 | 
						|
        NavigationDestination(
 | 
						|
          label: 'creatorHub'.tr(),
 | 
						|
          icon: const Icon(Symbols.ink_pen),
 | 
						|
        ),
 | 
						|
      if (wideScreen)
 | 
						|
        NavigationDestination(
 | 
						|
          label: 'developerHub'.tr(),
 | 
						|
          icon: const Icon(Symbols.data_object),
 | 
						|
        ),
 | 
						|
    ];
 | 
						|
 | 
						|
    int getCurrentIndex() {
 | 
						|
      if (currentLocation == '/') return 0;
 | 
						|
      final idx = kTabRoutes.indexWhere(
 | 
						|
        (p) => currentLocation.startsWith(p),
 | 
						|
        1,
 | 
						|
      );
 | 
						|
      final value = math.max(idx, 0);
 | 
						|
      return math.min(value, destinations.length - 1);
 | 
						|
    }
 | 
						|
 | 
						|
    void onDestinationSelected(int index) {
 | 
						|
      context.go(kTabRoutes[index]);
 | 
						|
    }
 | 
						|
 | 
						|
    final currentIndex = getCurrentIndex();
 | 
						|
 | 
						|
    final routes = kTabRoutes.sublist(
 | 
						|
      0,
 | 
						|
      isWideScreen(context) ? null : kWideScreenRouteStart,
 | 
						|
    );
 | 
						|
    final shouldShowFab = routes.contains(currentLocation) && !wideScreen;
 | 
						|
    final settings = ref.watch(appSettingsNotifierProvider);
 | 
						|
 | 
						|
    if (isWideScreen(context)) {
 | 
						|
      return Container(
 | 
						|
        color: Theme.of(context).colorScheme.surfaceContainer,
 | 
						|
        child: Row(
 | 
						|
          children: [
 | 
						|
            NavigationRail(
 | 
						|
              backgroundColor: Colors.transparent,
 | 
						|
              destinations:
 | 
						|
                  destinations
 | 
						|
                      .map(
 | 
						|
                        (e) => NavigationRailDestination(
 | 
						|
                          icon: e.icon,
 | 
						|
                          label: Text(e.label),
 | 
						|
                        ),
 | 
						|
                      )
 | 
						|
                      .toList(),
 | 
						|
              selectedIndex: currentIndex,
 | 
						|
              onDestinationSelected: onDestinationSelected,
 | 
						|
              trailingAtBottom: true,
 | 
						|
              trailing: const FabMenu(
 | 
						|
                elevation: 0,
 | 
						|
              ).padding(bottom: MediaQuery.of(context).padding.bottom + 16),
 | 
						|
            ),
 | 
						|
            Expanded(
 | 
						|
              child: ClipRRect(
 | 
						|
                borderRadius: const BorderRadius.only(
 | 
						|
                  topLeft: Radius.circular(16),
 | 
						|
                ),
 | 
						|
                child: child ?? const SizedBox.shrink(),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return Scaffold(
 | 
						|
      backgroundColor: Colors.transparent,
 | 
						|
      extendBody: true,
 | 
						|
      resizeToAvoidBottomInset: false,
 | 
						|
      body: ClipRRect(
 | 
						|
        borderRadius: const BorderRadius.only(
 | 
						|
          topLeft: Radius.circular(16),
 | 
						|
          topRight: Radius.circular(16),
 | 
						|
        ),
 | 
						|
        child: child ?? const SizedBox.shrink(),
 | 
						|
      ),
 | 
						|
      floatingActionButton: shouldShowFab ? const FabMenu() : null,
 | 
						|
      floatingActionButtonLocation:
 | 
						|
          shouldShowFab
 | 
						|
              ? _DockedFabLocation(context, settings.fabPosition)
 | 
						|
              : null,
 | 
						|
      bottomNavigationBar: ConditionalBottomNav(
 | 
						|
        child: ClipRRect(
 | 
						|
          borderRadius: BorderRadius.only(
 | 
						|
            topLeft: Radius.circular(16),
 | 
						|
            topRight: Radius.circular(16),
 | 
						|
          ),
 | 
						|
          child: MediaQuery.removePadding(
 | 
						|
            context: context,
 | 
						|
            removeTop: true,
 | 
						|
            child: BackdropFilter(
 | 
						|
              filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
 | 
						|
              child: BottomAppBar(
 | 
						|
                height: 56,
 | 
						|
                padding: EdgeInsets.symmetric(horizontal: 24),
 | 
						|
                shape: AutomaticNotchedShape(
 | 
						|
                  RoundedRectangleBorder(
 | 
						|
                    borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                color: Theme.of(context).colorScheme.surface.withOpacity(0.8),
 | 
						|
                child: Row(
 | 
						|
                  mainAxisSize: MainAxisSize.max,
 | 
						|
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
						|
                  children: () {
 | 
						|
                    final navItems =
 | 
						|
                        destinations.asMap().entries.map<Widget>((entry) {
 | 
						|
                          int index = entry.key;
 | 
						|
                          NavigationDestination dest = entry.value;
 | 
						|
                          return IconButton(
 | 
						|
                            icon: dest.icon,
 | 
						|
                            onPressed: () => onDestinationSelected(index),
 | 
						|
                            color:
 | 
						|
                                index == currentIndex
 | 
						|
                                    ? Theme.of(context).colorScheme.primary
 | 
						|
                                    : null,
 | 
						|
                          );
 | 
						|
                        }).toList();
 | 
						|
                    // Add mock item to leave space for FAB based on position
 | 
						|
                    final gapIndex = switch (settings.fabPosition) {
 | 
						|
                      'left' => 0,
 | 
						|
                      'right' => navItems.length,
 | 
						|
                      _ => navItems.length ~/ 2, // center
 | 
						|
                    };
 | 
						|
                    navItems.insert(
 | 
						|
                      gapIndex,
 | 
						|
                      SizedBox(
 | 
						|
                        width: settings.fabPosition == 'center' ? 72 : 48,
 | 
						|
                      ),
 | 
						|
                    );
 | 
						|
                    return navItems;
 | 
						|
                  }(),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _DockedFabLocation extends FloatingActionButtonLocation {
 | 
						|
  final BuildContext context;
 | 
						|
  final String fabPosition;
 | 
						|
 | 
						|
  const _DockedFabLocation(this.context, this.fabPosition);
 | 
						|
 | 
						|
  @override
 | 
						|
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
 | 
						|
    final mediaQuery = MediaQuery.of(context);
 | 
						|
    final safeAreaPadding = mediaQuery.padding;
 | 
						|
 | 
						|
    // Position horizontally based on setting
 | 
						|
    final double fabX = switch (fabPosition) {
 | 
						|
      'left' => scaffoldGeometry.minInsets.left + 24,
 | 
						|
      'right' =>
 | 
						|
        scaffoldGeometry.scaffoldSize.width -
 | 
						|
            scaffoldGeometry.floatingActionButtonSize.width -
 | 
						|
            scaffoldGeometry.minInsets.right -
 | 
						|
            24,
 | 
						|
      _ =>
 | 
						|
        (scaffoldGeometry.scaffoldSize.width -
 | 
						|
                scaffoldGeometry.floatingActionButtonSize.width) /
 | 
						|
            2, // center
 | 
						|
    };
 | 
						|
 | 
						|
    // Position closer to bottom with reduced padding
 | 
						|
    final double fabY =
 | 
						|
        scaffoldGeometry.scaffoldSize.height -
 | 
						|
        scaffoldGeometry.floatingActionButtonSize.height -
 | 
						|
        scaffoldGeometry.bottomSheetSize.height -
 | 
						|
        safeAreaPadding.bottom -
 | 
						|
        16;
 | 
						|
 | 
						|
    return Offset(fabX, fabY);
 | 
						|
  }
 | 
						|
}
 |