💄 Better sidebar navigation

This commit is contained in:
LittleSheep 2024-08-07 18:24:16 +08:00
parent 54dee9702b
commit 8009f4ca9b
5 changed files with 229 additions and 181 deletions

View File

@ -52,6 +52,17 @@ class Realm {
'is_community': isCommunity, 'is_community': isCommunity,
'account_id': accountId, 'account_id': accountId,
}; };
@override
bool operator ==(Object other) {
if (other is Realm) {
return other.id == id;
}
return false;
}
@override
int get hashCode => id;
} }
class RealmMember { class RealmMember {

View File

@ -3,16 +3,15 @@ import 'package:get/get.dart';
import 'package:solian/models/account_status.dart'; import 'package:solian/models/account_status.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_status_action.dart'; import 'package:solian/widgets/account/account_status_action.dart';
import 'package:solian/widgets/channel/channel_list.dart';
import 'package:solian/widgets/navigation/app_navigation.dart'; import 'package:solian/widgets/navigation/app_navigation.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:solian/widgets/navigation/app_navigation_regions.dart';
class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawer extends StatefulWidget {
final String? routeName; final String? routeName;
@ -24,12 +23,8 @@ class AppNavigationDrawer extends StatefulWidget {
} }
class _AppNavigationDrawerState extends State<AppNavigationDrawer> { class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
int? _selectedIndex = 0;
AccountStatus? _accountStatus; AccountStatus? _accountStatus;
late final ChannelProvider _channels;
Future<void> _getStatus() async { Future<void> _getStatus() async {
final StatusProvider provider = Get.find(); final StatusProvider provider = Get.find();
@ -41,15 +36,6 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}); });
} }
void _detectSelectedIndex() {
if (widget.routeName == null) return;
final nameList = AppNavigation.destinations.map((x) => x.page).toList();
final idx = nameList.indexOf(widget.routeName!);
_selectedIndex = idx != -1 ? idx : null;
}
void _closeDrawer() { void _closeDrawer() {
rootScaffoldKey.currentState!.closeDrawer(); rootScaffoldKey.currentState!.closeDrawer();
} }
@ -59,7 +45,6 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
onPressed: () { onPressed: () {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('settings');
setState(() => _selectedIndex = null);
_closeDrawer(); _closeDrawer();
}); });
} }
@ -67,175 +52,139 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_channels = Get.find();
_detectSelectedIndex();
_getStatus(); _getStatus();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_detectSelectedIndex();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
return NavigationDrawer( return Drawer(
backgroundColor: backgroundColor:
SolianTheme.isLargeScreen(context) ? Colors.transparent : null, SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
selectedIndex: _selectedIndex, child: SafeArea(
onDestinationSelected: (idx) { child: Column(
setState(() => _selectedIndex = idx); children: [
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page); Obx(() {
_closeDrawer(); if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
}, return ListTile(
children: [ contentPadding: const EdgeInsets.symmetric(horizontal: 28),
Obx(() { leading: const Icon(Icons.account_circle),
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) { title: Text('guest'.tr),
return ListTile( subtitle: Text('unsignedIn'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 28), trailing: _buildSettingButton(),
leading: const Icon(Icons.account_circle), onTap: () {
title: Text('guest'.tr), AppRouter.instance.goNamed('account');
subtitle: Text('unsignedIn'.tr), _closeDrawer();
trailing: _buildSettingButton(), },
onTap: () {
AppRouter.instance.goNamed('account');
setState(() => _selectedIndex = null);
_closeDrawer();
},
);
}
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(
auth.userProfile.value!['nick'],
maxLines: 1,
overflow: TextOverflow.fade,
),
subtitle: Builder(
builder: (context) {
if (_accountStatus == null) {
return Text('loading'.tr);
}
final info = StatusProvider.determineStatus(
_accountStatus!,
); );
return Text( }
info.$3,
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(
auth.userProfile.value!['nick'],
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
); ),
}, subtitle: Builder(
), builder: (context) {
leading: Obx(() { if (_accountStatus == null) {
final statusBadgeColor = _accountStatus != null return Text('loading'.tr);
? StatusProvider.determineStatus( }
final info = StatusProvider.determineStatus(
_accountStatus!, _accountStatus!,
).$2 );
: Colors.grey; return Text(
info.$3,
final RelationshipProvider relations = Get.find(); maxLines: 1,
final accountNotifications = relations.friendRequestCount.value; overflow: TextOverflow.fade,
);
return badges.Badge( },
badgeContent: Text(
accountNotifications.toString(),
style: const TextStyle(color: Colors.white),
), ),
showBadge: accountNotifications > 0, leading: Obx(() {
position: badges.BadgePosition.topEnd( final statusBadgeColor = _accountStatus != null
top: -10, ? StatusProvider.determineStatus(
end: -6, _accountStatus!,
), ).$2
child: badges.Badge( : Colors.grey;
showBadge: _accountStatus != null,
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
position: badges.BadgePosition.bottomEnd(
bottom: 0,
end: -2,
),
child: AccountAvatar(
content: auth.userProfile.value!['avatar'],
),
),
);
}),
trailing: _buildSettingButton(),
onTap: () {
AppRouter.instance.goNamed('account');
setState(() => _selectedIndex = null);
_closeDrawer();
},
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => AccountStatusAction(
currentStatus: _accountStatus!.status,
),
).then((val) {
if (val == true) _getStatus();
});
},
);
}).paddingOnly(top: 8),
const Divider(thickness: 0.3, height: 1).paddingOnly(
bottom: 12,
top: 8,
),
...AppNavigation.destinations.map(
(e) => NavigationDrawerDestination(
icon: e.icon,
label: Text(e.label),
),
),
const Divider(thickness: 0.3, height: 1).paddingOnly(
top: 12,
),
Obx(() {
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
return const SizedBox();
}
final selfId = auth.userProfile.value!['id']; final RelationshipProvider relations = Get.find();
final accountNotifications =
relations.friendRequestCount.value;
return Column( return badges.Badge(
children: [ badgeContent: Text(
Theme( accountNotifications.toString(),
data: Theme.of(context) style: const TextStyle(color: Colors.white),
.copyWith(dividerColor: Colors.transparent), ),
child: ExpansionTile( showBadge: accountNotifications > 0,
title: Text('channels'.tr), position: badges.BadgePosition.topEnd(
tilePadding: const EdgeInsets.symmetric(horizontal: 24), top: -10,
children: [ end: -6,
Obx( ),
() => SizedBox( child: badges.Badge(
height: 360, showBadge: _accountStatus != null,
child: RefreshIndicator( badgeStyle:
onRefresh: () => _channels.refreshAvailableChannel(), badges.BadgeStyle(badgeColor: statusBadgeColor),
child: ChannelListWidget( position: badges.BadgePosition.bottomEnd(
channels: _channels.groupChannels, bottom: 0,
selfId: selfId, end: -2,
isDense: true, ),
useReplace: true, child: AccountAvatar(
onSelected: (_) { content: auth.userProfile.value!['avatar'],
setState(() => _selectedIndex = null);
_closeDrawer();
},
),
),
), ),
), ),
], );
), }),
trailing: _buildSettingButton(),
onTap: () {
AppRouter.instance.goNamed('account');
_closeDrawer();
},
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => AccountStatusAction(
currentStatus: _accountStatus!.status,
),
).then((val) {
if (val == true) _getStatus();
});
},
);
}).paddingOnly(top: 8),
const Divider(thickness: 0.3, height: 1),
Column(
children: AppNavigation.destinations
.map(
(e) => ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
leading: e.icon,
title: Text(e.label),
enabled: true,
onTap: () {
AppRouter.instance.goNamed(e.page);
_closeDrawer();
},
),
)
.toList(),
).paddingSymmetric(vertical: 8),
const Divider(thickness: 0.3, height: 1),
Expanded(
child: AppNavigationRegions(
onSelected: (item) {
_closeDrawer();
},
), ),
], ),
); ],
}), ),
], ),
); );
} }
} }

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart';
import 'package:collection/collection.dart';
class AppNavigationRegions extends StatelessWidget {
final Function(Channel item) onSelected;
const AppNavigationRegions({super.key, required this.onSelected});
void _gotoChannel(Channel item) {
AppRouter.instance.pushReplacementNamed(
'channelChat',
pathParameters: {'alias': item.alias},
queryParameters: {
if (item.realmId != null) 'realm': item.realm!.alias,
},
);
onSelected(item);
}
Widget _buildEntry(BuildContext context, Channel item) {
const padding = EdgeInsets.symmetric(horizontal: 20);
return ListTile(
minTileHeight: 0,
leading: const Icon(Icons.tag_outlined),
contentPadding: padding,
title: Text(item.name),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => _gotoChannel(item),
);
}
@override
Widget build(BuildContext context) {
final ChannelProvider channels = Get.find();
return Obx(() {
final List<Channel> noRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId == null)
.toList();
final List<Channel> hasRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId != null)
.toList();
return CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 8)),
SliverList.builder(
itemCount: noRealmGroupChannels.length,
itemBuilder: (context, index) {
final element = noRealmGroupChannels[index];
return _buildEntry(context, element);
},
),
SliverList.list(
children: hasRealmGroupChannels
.groupListsBy((x) => x.realm)
.entries
.map((element) {
return ExpansionTile(
minTileHeight: 0,
tilePadding: const EdgeInsets.only(left: 20, right: 24),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
title: Text(element.value.first.realm!.name),
leading: const Icon(Icons.workspaces, size: 16)
.paddingSymmetric(horizontal: 4),
children:
element.value.map((x) => _buildEntry(context, x)).toList(),
);
}).toList(),
),
],
);
});
}
}

View File

@ -226,7 +226,7 @@ packages:
source: hosted source: hosted
version: "4.10.0" version: "4.10.0"
collection: collection:
dependency: transitive dependency: "direct main"
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
@ -405,10 +405,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.6" version: "8.0.7"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -740,10 +740,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: f46bd76cef6e8d787dc707d0c591f0e89c912a2970c7b5e68a55b9cca1bdde4c sha256: "67faa07cf49392b50b1aa14590a83caa64d2109345fabd29899dcd8da8538348"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.6" version: "0.11.6+hotfix.1"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -788,10 +788,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722 sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.2" version: "14.2.3"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@ -876,10 +876,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: "3fe99e3a459b969f657565a853353bdea7e3d3cae34f7dd124836267d426ec87" sha256: c0e72ecd170b00a5590bb71238d57dc8ad22ee14c60c6b0d1a4e05cafbc5db4b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.12+10" version: "0.8.12+11"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@ -1276,10 +1276,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54 sha256: eaf2a1ec4472775451e88ca6a7b86559ef2f1d1ed903942ed135e38ea0097dca
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.0.7" version: "12.0.8"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
@ -1300,10 +1300,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.1" version: "4.2.2"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
@ -1897,10 +1897,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049 sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.3"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:

View File

@ -68,6 +68,7 @@ dependencies:
async: ^2.11.0 async: ^2.11.0
field_suggestion: ^0.2.5 field_suggestion: ^0.2.5
flutter_typeahead: ^5.2.0 flutter_typeahead: ^5.2.0
collection: ^1.18.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: