✨ Brand new app navigation region
This commit is contained in:
		@@ -12,9 +12,12 @@ class Realm {
 | 
			
		||||
  String alias;
 | 
			
		||||
  String name;
 | 
			
		||||
  String description;
 | 
			
		||||
  String? avatar;
 | 
			
		||||
  String? banner;
 | 
			
		||||
  bool isPublic;
 | 
			
		||||
  bool isCommunity;
 | 
			
		||||
  int? accountId;
 | 
			
		||||
  int? externalId;
 | 
			
		||||
 | 
			
		||||
  Realm({
 | 
			
		||||
    required this.id,
 | 
			
		||||
@@ -24,9 +27,12 @@ class Realm {
 | 
			
		||||
    required this.alias,
 | 
			
		||||
    required this.name,
 | 
			
		||||
    required this.description,
 | 
			
		||||
    required this.avatar,
 | 
			
		||||
    required this.banner,
 | 
			
		||||
    required this.isPublic,
 | 
			
		||||
    required this.isCommunity,
 | 
			
		||||
    this.accountId,
 | 
			
		||||
    this.externalId,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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,
 | 
			
		||||
      name: json['name'] as String,
 | 
			
		||||
      description: json['description'] as String,
 | 
			
		||||
      avatar: json['avatar'] as String?,
 | 
			
		||||
      banner: json['banner'] as String?,
 | 
			
		||||
      isPublic: json['is_public'] as bool,
 | 
			
		||||
      isCommunity: json['is_community'] as bool,
 | 
			
		||||
      accountId: (json['account_id'] as num?)?.toInt(),
 | 
			
		||||
      externalId: (json['external_id'] as num?)?.toInt(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
 | 
			
		||||
@@ -29,9 +32,12 @@ Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
 | 
			
		||||
      'alias': instance.alias,
 | 
			
		||||
      'name': instance.name,
 | 
			
		||||
      'description': instance.description,
 | 
			
		||||
      'avatar': instance.avatar,
 | 
			
		||||
      'banner': instance.banner,
 | 
			
		||||
      'is_public': instance.isPublic,
 | 
			
		||||
      'is_community': instance.isCommunity,
 | 
			
		||||
      'account_id': instance.accountId,
 | 
			
		||||
      'external_id': instance.externalId,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
RealmMember _$RealmMemberFromJson(Map<String, dynamic> json) => RealmMember(
 | 
			
		||||
 
 | 
			
		||||
@@ -315,7 +315,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
			
		||||
                  Card(
 | 
			
		||||
                    child: ListTile(
 | 
			
		||||
                      contentPadding:
 | 
			
		||||
                          const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                          const EdgeInsets.only(left: 24, right: 32),
 | 
			
		||||
                      trailing: const Icon(Icons.inbox_outlined),
 | 
			
		||||
                      title: Text('notifyEmpty'.tr),
 | 
			
		||||
                      subtitle: Text('notifyEmptyCaption'.tr),
 | 
			
		||||
@@ -368,6 +368,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
			
		||||
                      return SizedBox(
 | 
			
		||||
                        width: min(480, width),
 | 
			
		||||
                        child: Card(
 | 
			
		||||
                          child: ClipRRect(
 | 
			
		||||
                            borderRadius: const BorderRadius.all(
 | 
			
		||||
                              Radius.circular(8),
 | 
			
		||||
                            ),
 | 
			
		||||
                            child: SingleChildScrollView(
 | 
			
		||||
                              child: PostListEntryWidget(
 | 
			
		||||
                                item: item,
 | 
			
		||||
@@ -382,6 +386,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
 | 
			
		||||
                                    .surfaceContainerLow,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ).paddingSymmetric(horizontal: 8),
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
 
 | 
			
		||||
@@ -162,7 +162,13 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
 | 
			
		||||
        ),
 | 
			
		||||
        contentPadding: padding,
 | 
			
		||||
        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),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -239,45 +239,38 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
 | 
			
		||||
          children: [
 | 
			
		||||
            _buildUserInfo().paddingSymmetric(vertical: 8),
 | 
			
		||||
            const Divider(thickness: 0.3, height: 1),
 | 
			
		||||
            Column(
 | 
			
		||||
            SizedBox(
 | 
			
		||||
              width: double.infinity,
 | 
			
		||||
              child: Wrap(
 | 
			
		||||
                runSpacing: 8,
 | 
			
		||||
                spacing: 8,
 | 
			
		||||
                alignment: WrapAlignment.spaceAround,
 | 
			
		||||
                children: AppNavigation.destinations
 | 
			
		||||
                    .map(
 | 
			
		||||
                    (e) => _isCollapsed
 | 
			
		||||
                        ? Tooltip(
 | 
			
		||||
                      (e) => Card(
 | 
			
		||||
                        elevation: 0,
 | 
			
		||||
                        margin: EdgeInsets.zero,
 | 
			
		||||
                        child: Tooltip(
 | 
			
		||||
                          message: e.label,
 | 
			
		||||
                          child: InkWell(
 | 
			
		||||
                              child: Icon(e.icon, size: 20).paddingSymmetric(
 | 
			
		||||
                                horizontal: 28,
 | 
			
		||||
                                vertical: 16,
 | 
			
		||||
                              ),
 | 
			
		||||
                            borderRadius:
 | 
			
		||||
                                const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                            child: Icon(e.icon, size: 20).paddingAll(20),
 | 
			
		||||
                            onTap: () {
 | 
			
		||||
                              AppRouter.instance.goNamed(e.page);
 | 
			
		||||
                              _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(),
 | 
			
		||||
              ).paddingSymmetric(vertical: 8, horizontal: 12),
 | 
			
		||||
            ),
 | 
			
		||||
            const Divider(thickness: 0.3, height: 1),
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: AppNavigationRegion(
 | 
			
		||||
                isCollapsed: _isCollapsed,
 | 
			
		||||
                onSelected: (item) {
 | 
			
		||||
                  _closeDrawer();
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            const Divider(thickness: 0.3, height: 1),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +1,126 @@
 | 
			
		||||
import 'package:flutter/material.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/router.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:solian/providers/content/realm.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 Function(Channel item) onSelected;
 | 
			
		||||
 | 
			
		||||
  const AppNavigationRegion({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.onSelected,
 | 
			
		||||
    this.isCollapsed = false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  void _gotoChannel(Channel item) {
 | 
			
		||||
    AppRouter.instance.goNamed(
 | 
			
		||||
      'channelChat',
 | 
			
		||||
      pathParameters: {'alias': item.alias},
 | 
			
		||||
      queryParameters: {
 | 
			
		||||
        if (item.realmId != null) 'realm': item.realm!.alias,
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    onSelected(item);
 | 
			
		||||
  @override
 | 
			
		||||
  State<AppNavigationRegion> createState() => _AppNavigationRegionState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  Widget _buildEntry(BuildContext context, Channel 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);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
    if (isCollapsed) {
 | 
			
		||||
    if (widget.isCollapsed) {
 | 
			
		||||
      return InkWell(
 | 
			
		||||
        child: const Icon(Icons.tag_outlined, size: 20).paddingSymmetric(
 | 
			
		||||
          horizontal: 20,
 | 
			
		||||
          vertical: 16,
 | 
			
		||||
        ),
 | 
			
		||||
        onTap: () => _gotoChannel(item),
 | 
			
		||||
        child: _buildEntryAvatar(item),
 | 
			
		||||
        onTap: () => _focusRealm(item),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ListTile(
 | 
			
		||||
      minTileHeight: 0,
 | 
			
		||||
      leading: const Icon(Icons.tag_outlined),
 | 
			
		||||
      leading: _buildEntryAvatar(item),
 | 
			
		||||
      contentPadding: padding,
 | 
			
		||||
      title: Text(item.name),
 | 
			
		||||
      subtitle: Text(
 | 
			
		||||
@@ -50,33 +128,34 @@ class AppNavigationRegion extends StatelessWidget {
 | 
			
		||||
        maxLines: 1,
 | 
			
		||||
        overflow: TextOverflow.ellipsis,
 | 
			
		||||
      ),
 | 
			
		||||
      onTap: () => _gotoChannel(item),
 | 
			
		||||
      onTap: () => _focusRealm(item),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final RealmProvider realms = Get.find();
 | 
			
		||||
    final ChannelProvider channels = Get.find();
 | 
			
		||||
    final AuthProvider auth = 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();
 | 
			
		||||
 | 
			
		||||
      if (isCollapsed) {
 | 
			
		||||
    return AnimatedBuilder(
 | 
			
		||||
      animation: _animationController,
 | 
			
		||||
      builder: (context, child) {
 | 
			
		||||
        return SlideTransition(
 | 
			
		||||
          position: _animationTween,
 | 
			
		||||
          child: child,
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
      child: _focusedRealm == null
 | 
			
		||||
          ? Obx(() {
 | 
			
		||||
              if (widget.isCollapsed) {
 | 
			
		||||
                return CustomScrollView(
 | 
			
		||||
                  slivers: [
 | 
			
		||||
                    const SliverPadding(padding: EdgeInsets.only(top: 8)),
 | 
			
		||||
                    SliverList.builder(
 | 
			
		||||
              itemCount:
 | 
			
		||||
                  noRealmGroupChannels.length + hasRealmGroupChannels.length,
 | 
			
		||||
                      itemCount: realms.availableRealms.length,
 | 
			
		||||
                      itemBuilder: (context, index) {
 | 
			
		||||
                final element = index >= noRealmGroupChannels.length
 | 
			
		||||
                    ? hasRealmGroupChannels[index - noRealmGroupChannels.length]
 | 
			
		||||
                    : noRealmGroupChannels[index];
 | 
			
		||||
                        final element = realms.availableRealms[index];
 | 
			
		||||
                        return Tooltip(
 | 
			
		||||
                          message: element.name,
 | 
			
		||||
                          child: _buildEntry(context, element),
 | 
			
		||||
@@ -91,35 +170,55 @@ class AppNavigationRegion extends StatelessWidget {
 | 
			
		||||
                slivers: [
 | 
			
		||||
                  const SliverPadding(padding: EdgeInsets.only(top: 8)),
 | 
			
		||||
                  SliverList.builder(
 | 
			
		||||
            itemCount: noRealmGroupChannels.length,
 | 
			
		||||
                    itemCount: realms.availableRealms.length,
 | 
			
		||||
                    itemBuilder: (context, index) {
 | 
			
		||||
              final element = noRealmGroupChannels[index];
 | 
			
		||||
                      final element = realms.availableRealms[index];
 | 
			
		||||
                      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)),
 | 
			
		||||
                ],
 | 
			
		||||
              );
 | 
			
		||||
    });
 | 
			
		||||
            })
 | 
			
		||||
          : 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">
 | 
			
		||||
    
 | 
			
		||||
  
 | 
			
		||||
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
 | 
			
		||||
  <style id="splash-screen-style">
 | 
			
		||||
    html {
 | 
			
		||||
      height: 100%
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user