Drawer navigation

This commit is contained in:
LittleSheep 2024-11-14 00:08:09 +08:00
parent 2c6b1feca6
commit 9c8dad0176
11 changed files with 274 additions and 48 deletions

View File

@ -15,6 +15,8 @@
"screenAccountPublisherEdit": "Edit Publisher",
"screenAccountProfileEdit": "Edit Profile",
"screenSettings": "Settings",
"screenAlbum": "Album",
"screenChat": "Chat",
"dialogOkay": "Okay",
"dialogCancel": "Cancel",
"dialogConfirm": "Confirm",

View File

@ -15,6 +15,8 @@
"screenAccountPublisherEdit": "编辑发布者",
"screenAccountProfileEdit": "编辑资料",
"screenSettings": "设置",
"screenAlbum": "相册",
"screenChat": "聊天",
"dialogOkay": "好的",
"dialogCancel": "取消",
"dialogConfirm": "确认",

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/theme.dart';
@ -39,6 +40,7 @@ class SolianApp extends StatelessWidget {
providers: [
Provider(create: (_) => SnNetworkProvider()),
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NavigationProvider()),
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
],
@ -59,6 +61,7 @@ class AppMainContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.read<NavigationProvider>();
context.read<UserProvider>();
final th = context.watch<ThemeProvider>();

View File

@ -0,0 +1,112 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppNavDestination {
final String label;
final String screen;
final Widget icon;
final bool isPinned;
const AppNavDestination({
required this.label,
required this.screen,
required this.icon,
this.isPinned = false,
});
}
class NavigationProvider extends ChangeNotifier {
int? _currentIndex;
int? get currentIndex => _currentIndex;
static const List<AppNavDestination> kAllDestination = [
AppNavDestination(
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
screen: 'home',
label: 'screenHome',
),
AppNavDestination(
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
screen: 'explore',
label: 'screenExplore',
),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: 'screenAccount',
),
AppNavDestination(
icon: Icon(Symbols.album, weight: 400, opticalSize: 20),
screen: 'album',
label: 'screenAlbum',
),
AppNavDestination(
icon: Icon(Symbols.chat, weight: 400, opticalSize: 20),
screen: 'chat',
label: 'screenChat',
),
];
static const List<String> kDefaultPinnedDestination = [
'home',
'explore',
'account'
];
List<AppNavDestination> destinations = [];
int get pinnedDestinationCount =>
destinations.where((ele) => ele.isPinned).length;
NavigationProvider() {
buildDestinations(kDefaultPinnedDestination);
SharedPreferences.getInstance().then((prefs) {
final pinned = prefs.getStringList("app_pinned_navigation");
if (pinned != null) buildDestinations(pinned);
});
}
void buildDestinations(List<String> pinned) {
destinations = kAllDestination
.map(
(ele) => AppNavDestination(
label: ele.label,
screen: ele.screen,
icon: ele.icon,
isPinned: pinned.contains(ele.screen),
),
)
.toList();
notifyListeners();
}
int getIndexInRange(int min, int max) {
return math.max(min, math.min(_currentIndex ?? 0, max));
}
bool isIndexInRange(int min, int max) {
return _currentIndex != null &&
_currentIndex! >= min &&
_currentIndex! < max;
}
void autoDetectIndex(GoRouter? state) {
if (state == null) return;
final idx = destinations.indexWhere(
(ele) =>
ele.screen ==
state.routerDelegate.currentConfiguration.last.route.name,
);
_currentIndex = idx == -1 ? null : idx;
notifyListeners();
}
void setIndex(int idx) {
_currentIndex = idx;
notifyListeners();
}
}

View File

@ -4,8 +4,10 @@ import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart';
import 'package:surface/screens/account/publishers/publishers.dart';
import 'package:surface/screens/album.dart';
import 'package:surface/screens/auth/login.dart';
import 'package:surface/screens/auth/register.dart';
import 'package:surface/screens/chat.dart';
import 'package:surface/screens/explore.dart';
import 'package:surface/screens/home.dart';
import 'package:surface/screens/post/post_detail.dart';
@ -20,6 +22,7 @@ final appRouter = GoRouter(
builder: (context, state, child) => AppScaffold(
body: child,
showBottomNavigation: true,
showDrawer: true,
),
routes: [
GoRoute(
@ -37,6 +40,16 @@ final appRouter = GoRouter(
name: 'account',
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) => const ChatScreen(),
),
GoRoute(
path: '/album',
name: 'album',
builder: (context, state) => const AlbumScreen(),
),
],
),
ShellRoute(
@ -74,6 +87,7 @@ final appRouter = GoRouter(
builder: (context, state, child) => AppScaffold(
body: child,
autoImplyAppBar: true,
showDrawer: true,
),
routes: [
GoRoute(

10
lib/screens/album.dart Normal file
View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class AlbumScreen extends StatelessWidget {
const AlbumScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

10
lib/screens/chat.dart Normal file
View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class ChatScreen extends StatelessWidget {
const ChatScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -1,6 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:surface/widgets/navigation/app_destinations.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/navigation.dart';
class AppBottomNavigationBar extends StatefulWidget {
const AppBottomNavigationBar({super.key});
@ -10,23 +12,46 @@ class AppBottomNavigationBar extends StatefulWidget {
}
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
int _currentIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
});
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed,
showUnselectedLabels: false,
items: appDestinations.map((ele) {
return BottomNavigationBarItem(
icon: ele.icon,
label: ele.label,
final nav = context.watch<NavigationProvider>();
return ListenableBuilder(
listenable: nav,
builder: (context, _) {
if (!nav.isIndexInRange(0, nav.pinnedDestinationCount)) {
return const SizedBox.shrink();
}
final destinations = [
...nav.destinations.where((ele) => ele.isPinned),
];
return BottomNavigationBar(
currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
type: BottomNavigationBarType.fixed,
showUnselectedLabels: false,
items: destinations.map((ele) {
return BottomNavigationBarItem(
icon: ele.icon,
label: ele.label.tr(),
);
}).toList(),
onTap: (idx) {
nav.setIndex(idx);
GoRouter.of(context).goNamed(destinations[idx].screen);
},
);
}).toList(),
onTap: (idx) {
setState(() => _currentIndex = idx);
GoRouter.of(context).goNamed(appDestinations[idx].screen);
},
);
}

View File

@ -1,33 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
class AppNavDestination {
final String label;
final String screen;
final Widget icon;
AppNavDestination({
required this.label,
required this.screen,
required this.icon,
});
}
List<AppNavDestination> appDestinations = [
AppNavDestination(
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
screen: 'home',
label: tr('screenHome'),
),
AppNavDestination(
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
screen: 'explore',
label: tr('screenExplore'),
),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: tr('screenAccount'),
),
];

View File

@ -0,0 +1,76 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/navigation.dart';
class AppNavigationDrawer extends StatefulWidget {
const AppNavigationDrawer({super.key});
@override
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
}
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
});
}
@override
Widget build(BuildContext context) {
final nav = context.watch<NavigationProvider>();
return ListenableBuilder(
listenable: nav,
builder: (context, _) {
final destinations = [
...nav.destinations.where((ele) => ele.isPinned),
...nav.destinations.where((ele) => !ele.isPinned),
];
return NavigationDrawer(
selectedIndex: nav.currentIndex,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
Text('Solar Network 2.0α').fontSize(12).textColor(
Theme.of(context).colorScheme.onSurface.withOpacity(0.5)),
],
).padding(
horizontal: 32,
vertical: 8,
),
...destinations.where((ele) => ele.isPinned).map((ele) {
return NavigationDrawerDestination(
icon: ele.icon,
label: Text(ele.label).tr(),
);
}),
const Divider(),
...destinations.where((ele) => !ele.isPinned).map((ele) {
return NavigationDrawerDestination(
icon: ele.icon,
label: Text(ele.label).tr(),
);
}),
],
onDestinationSelected: (idx) {
nav.setIndex(idx);
GoRouter.of(context).goNamed(destinations[idx].screen);
Scaffold.of(context).closeDrawer();
},
);
},
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
class AppScaffold extends StatelessWidget {
final PreferredSizeWidget? appBar;
@ -14,6 +15,7 @@ class AppScaffold extends StatelessWidget {
final Widget? body;
final bool autoImplyAppBar;
final bool showBottomNavigation;
final bool showDrawer;
const AppScaffold({
super.key,
this.appBar,
@ -23,12 +25,14 @@ class AppScaffold extends StatelessWidget {
this.body,
this.autoImplyAppBar = false,
this.showBottomNavigation = false,
this.showDrawer = false,
});
@override
Widget build(BuildContext context) {
final isShowDrawer = showDrawer;
final isShowBottomNavigation = (showBottomNavigation)
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
? (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
: false;
final state = GoRouter.maybeOf(context);
@ -50,6 +54,7 @@ class AppScaffold extends StatelessWidget {
body: body,
floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButton: floatingActionButton,
drawer: isShowDrawer ? AppNavigationDrawer() : null,
bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null,
),