✨ Brand new app navigation region
This commit is contained in:
parent
19ec0a7ede
commit
6daa04c208
@ -12,9 +12,12 @@ class Realm {
|
|||||||
String alias;
|
String alias;
|
||||||
String name;
|
String name;
|
||||||
String description;
|
String description;
|
||||||
|
String? avatar;
|
||||||
|
String? banner;
|
||||||
bool isPublic;
|
bool isPublic;
|
||||||
bool isCommunity;
|
bool isCommunity;
|
||||||
int? accountId;
|
int? accountId;
|
||||||
|
int? externalId;
|
||||||
|
|
||||||
Realm({
|
Realm({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -24,9 +27,12 @@ class Realm {
|
|||||||
required this.alias,
|
required this.alias,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
|
required this.avatar,
|
||||||
|
required this.banner,
|
||||||
required this.isPublic,
|
required this.isPublic,
|
||||||
required this.isCommunity,
|
required this.isCommunity,
|
||||||
this.accountId,
|
this.accountId,
|
||||||
|
this.externalId,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Realm.fromJson(Map<String, dynamic> json) => _$RealmFromJson(json);
|
factory Realm.fromJson(Map<String, dynamic> json) => _$RealmFromJson(json);
|
||||||
|
@ -16,9 +16,12 @@ Realm _$RealmFromJson(Map<String, dynamic> json) => Realm(
|
|||||||
alias: json['alias'] as String,
|
alias: json['alias'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
description: json['description'] as String,
|
description: json['description'] as String,
|
||||||
|
avatar: json['avatar'] as String?,
|
||||||
|
banner: json['banner'] as String?,
|
||||||
isPublic: json['is_public'] as bool,
|
isPublic: json['is_public'] as bool,
|
||||||
isCommunity: json['is_community'] as bool,
|
isCommunity: json['is_community'] as bool,
|
||||||
accountId: (json['account_id'] as num?)?.toInt(),
|
accountId: (json['account_id'] as num?)?.toInt(),
|
||||||
|
externalId: (json['external_id'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
|
Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
|
||||||
@ -29,9 +32,12 @@ Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
|
|||||||
'alias': instance.alias,
|
'alias': instance.alias,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'description': instance.description,
|
'description': instance.description,
|
||||||
|
'avatar': instance.avatar,
|
||||||
|
'banner': instance.banner,
|
||||||
'is_public': instance.isPublic,
|
'is_public': instance.isPublic,
|
||||||
'is_community': instance.isCommunity,
|
'is_community': instance.isCommunity,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
|
'external_id': instance.externalId,
|
||||||
};
|
};
|
||||||
|
|
||||||
RealmMember _$RealmMemberFromJson(Map<String, dynamic> json) => RealmMember(
|
RealmMember _$RealmMemberFromJson(Map<String, dynamic> json) => RealmMember(
|
||||||
|
@ -315,7 +315,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
Card(
|
Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 24),
|
const EdgeInsets.only(left: 24, right: 32),
|
||||||
trailing: const Icon(Icons.inbox_outlined),
|
trailing: const Icon(Icons.inbox_outlined),
|
||||||
title: Text('notifyEmpty'.tr),
|
title: Text('notifyEmpty'.tr),
|
||||||
subtitle: Text('notifyEmptyCaption'.tr),
|
subtitle: Text('notifyEmptyCaption'.tr),
|
||||||
@ -368,6 +368,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: min(480, width),
|
width: min(480, width),
|
||||||
child: Card(
|
child: Card(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: PostListEntryWidget(
|
child: PostListEntryWidget(
|
||||||
item: item,
|
item: item,
|
||||||
@ -382,6 +386,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
.surfaceContainerLow,
|
.surfaceContainerLow,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
).paddingSymmetric(horizontal: 8),
|
).paddingSymmetric(horizontal: 8),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -162,7 +162,13 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
),
|
),
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(item.name),
|
title: Text(item.name),
|
||||||
subtitle: !widget.isDense ? Text(item.description) : null,
|
subtitle: !widget.isDense
|
||||||
|
? Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -239,45 +239,38 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
|
|||||||
children: [
|
children: [
|
||||||
_buildUserInfo().paddingSymmetric(vertical: 8),
|
_buildUserInfo().paddingSymmetric(vertical: 8),
|
||||||
const Divider(thickness: 0.3, height: 1),
|
const Divider(thickness: 0.3, height: 1),
|
||||||
Column(
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Wrap(
|
||||||
|
runSpacing: 8,
|
||||||
|
spacing: 8,
|
||||||
|
alignment: WrapAlignment.spaceAround,
|
||||||
children: AppNavigation.destinations
|
children: AppNavigation.destinations
|
||||||
.map(
|
.map(
|
||||||
(e) => _isCollapsed
|
(e) => Card(
|
||||||
? Tooltip(
|
elevation: 0,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Tooltip(
|
||||||
message: e.label,
|
message: e.label,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
child: Icon(e.icon, size: 20).paddingSymmetric(
|
borderRadius:
|
||||||
horizontal: 28,
|
const BorderRadius.all(Radius.circular(8)),
|
||||||
vertical: 16,
|
child: Icon(e.icon, size: 20).paddingAll(20),
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.goNamed(e.page);
|
AppRouter.instance.goNamed(e.page);
|
||||||
_closeDrawer();
|
_closeDrawer();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
|
||||||
: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
),
|
),
|
||||||
leading: Icon(e.icon, size: 20).paddingAll(2),
|
|
||||||
title: !_isCollapsed ? Text(e.label) : null,
|
|
||||||
enabled: true,
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed(e.page);
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
).paddingSymmetric(vertical: 8, horizontal: 12),
|
||||||
),
|
),
|
||||||
const Divider(thickness: 0.3, height: 1),
|
const Divider(thickness: 0.3, height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AppNavigationRegion(
|
child: AppNavigationRegion(
|
||||||
isCollapsed: _isCollapsed,
|
isCollapsed: _isCollapsed,
|
||||||
onSelected: (item) {
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(thickness: 0.3, height: 1),
|
const Divider(thickness: 0.3, height: 1),
|
||||||
|
@ -1,48 +1,126 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
import 'package:solian/providers/content/channel.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/providers/content/realm.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:solian/widgets/channel/channel_list.dart';
|
||||||
|
|
||||||
class AppNavigationRegion extends StatelessWidget {
|
class AppNavigationRegion extends StatefulWidget {
|
||||||
final bool isCollapsed;
|
final bool isCollapsed;
|
||||||
final Function(Channel item) onSelected;
|
|
||||||
|
|
||||||
const AppNavigationRegion({
|
const AppNavigationRegion({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onSelected,
|
|
||||||
this.isCollapsed = false,
|
this.isCollapsed = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
void _gotoChannel(Channel item) {
|
@override
|
||||||
AppRouter.instance.goNamed(
|
State<AppNavigationRegion> createState() => _AppNavigationRegionState();
|
||||||
'channelChat',
|
}
|
||||||
pathParameters: {'alias': item.alias},
|
|
||||||
queryParameters: {
|
|
||||||
if (item.realmId != null) 'realm': item.realm!.alias,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onSelected(item);
|
class _AppNavigationRegionState extends State<AppNavigationRegion>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
Realm? _focusedRealm;
|
||||||
|
|
||||||
|
bool _isTryingExit = false;
|
||||||
|
|
||||||
|
late final AnimationController _animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
);
|
||||||
|
late final Animation<Offset> _animationTween = Tween<Offset>(
|
||||||
|
begin: Offset.zero,
|
||||||
|
end: const Offset(1.0, 0.0),
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
));
|
||||||
|
|
||||||
|
void _focusRealm(Realm item) {
|
||||||
|
_animationController.animateTo(1).then((_) {
|
||||||
|
setState(() => _focusedRealm = item);
|
||||||
|
_animationController.animateTo(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEntry(BuildContext context, Channel item) {
|
void _unFocusRealm() {
|
||||||
|
_animationController.animateTo(1).then((_) {
|
||||||
|
setState(() => _focusedRealm = null);
|
||||||
|
_animationController.animateTo(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRealmFocusAvatar() {
|
||||||
|
return MouseRegion(
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
|
switchOutCurve: Curves.fastOutSlowIn,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _isTryingExit
|
||||||
|
? GestureDetector(
|
||||||
|
child: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
).paddingSymmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
onTap: () => _unFocusRealm(),
|
||||||
|
)
|
||||||
|
: _buildEntryAvatar(_focusedRealm!),
|
||||||
|
),
|
||||||
|
onEnter: (_) => setState(() => _isTryingExit = true),
|
||||||
|
onExit: (_) => setState(() => _isTryingExit = false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntryAvatar(Realm item) {
|
||||||
|
return Hero(
|
||||||
|
tag: Key('region-realm-avatar-${item.id}'),
|
||||||
|
child: (item.avatar?.isNotEmpty ?? false)
|
||||||
|
? AccountAvatar(content: item.avatar)
|
||||||
|
: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
).paddingSymmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntry(BuildContext context, Realm item) {
|
||||||
const padding = EdgeInsets.symmetric(horizontal: 20);
|
const padding = EdgeInsets.symmetric(horizontal: 20);
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (widget.isCollapsed) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
child: const Icon(Icons.tag_outlined, size: 20).paddingSymmetric(
|
child: _buildEntryAvatar(item),
|
||||||
horizontal: 20,
|
onTap: () => _focusRealm(item),
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
minTileHeight: 0,
|
minTileHeight: 0,
|
||||||
leading: const Icon(Icons.tag_outlined),
|
leading: _buildEntryAvatar(item),
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(item.name),
|
title: Text(item.name),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
@ -50,33 +128,34 @@ class AppNavigationRegion extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _focusRealm(item),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
final ChannelProvider channels = Get.find();
|
final ChannelProvider channels = Get.find();
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
return Obx(() {
|
return AnimatedBuilder(
|
||||||
final List<Channel> noRealmGroupChannels = channels.availableChannels
|
animation: _animationController,
|
||||||
.where((x) => x.type == 0 && x.realmId == null)
|
builder: (context, child) {
|
||||||
.toList();
|
return SlideTransition(
|
||||||
final List<Channel> hasRealmGroupChannels = channels.availableChannels
|
position: _animationTween,
|
||||||
.where((x) => x.type == 0 && x.realmId != null)
|
child: child,
|
||||||
.toList();
|
);
|
||||||
|
},
|
||||||
if (isCollapsed) {
|
child: _focusedRealm == null
|
||||||
|
? Obx(() {
|
||||||
|
if (widget.isCollapsed) {
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount:
|
itemCount: realms.availableRealms.length,
|
||||||
noRealmGroupChannels.length + hasRealmGroupChannels.length,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final element = index >= noRealmGroupChannels.length
|
final element = realms.availableRealms[index];
|
||||||
? hasRealmGroupChannels[index - noRealmGroupChannels.length]
|
|
||||||
: noRealmGroupChannels[index];
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: element.name,
|
message: element.name,
|
||||||
child: _buildEntry(context, element),
|
child: _buildEntry(context, element),
|
||||||
@ -91,35 +170,55 @@ class AppNavigationRegion extends StatelessWidget {
|
|||||||
slivers: [
|
slivers: [
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: noRealmGroupChannels.length,
|
itemCount: realms.availableRealms.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final element = noRealmGroupChannels[index];
|
final element = realms.availableRealms[index];
|
||||||
return _buildEntry(context, element);
|
return _buildEntry(context, element);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SliverList.list(
|
|
||||||
children: hasRealmGroupChannels
|
|
||||||
.groupListsBy((x) => x.realm)
|
|
||||||
.entries
|
|
||||||
.map((element) {
|
|
||||||
return ExpansionTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
initiallyExpanded: true,
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
|
const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
})
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
if (widget.isCollapsed)
|
||||||
|
Tooltip(
|
||||||
|
message: _focusedRealm!.name,
|
||||||
|
child: _buildRealmFocusAvatar().paddingSymmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 0,
|
||||||
|
tileColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
leading: _buildRealmFocusAvatar(),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
title: Text(_focusedRealm!.name),
|
||||||
|
subtitle: Text(
|
||||||
|
_focusedRealm!.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(
|
||||||
|
() => ChannelListWidget(
|
||||||
|
channels: channels.availableChannels
|
||||||
|
.where(
|
||||||
|
(x) => x.realm?.externalId == _focusedRealm?.id,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
selfId: auth.userProfile.value!['id'],
|
||||||
|
noCategory: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,6 @@
|
|||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
|
||||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
|
||||||
<style id="splash-screen-style">
|
<style id="splash-screen-style">
|
||||||
html {
|
html {
|
||||||
height: 100%
|
height: 100%
|
||||||
|
Loading…
Reference in New Issue
Block a user