Compare commits
	
		
			4 Commits
		
	
	
		
			a1c4e5eca0
			...
			908f0cb59e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 908f0cb59e | |||
| 7c2b8de931 | |||
| 6bb9c21759 | |||
| 8f2fc55608 | 
							
								
								
									
										
											BIN
										
									
								
								assets/audio/sfx/launch-done.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/sfx/launch-done.mp3
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -939,5 +939,7 @@
 | 
			
		||||
  "settingsSoundEffects": "Sound Effects",
 | 
			
		||||
  "settingsSoundEffectsDescription": "Enable the sound effects around the app.",
 | 
			
		||||
  "settingsResetMemorizedWindowSize": "Reset Window Size",
 | 
			
		||||
  "settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size."
 | 
			
		||||
  "settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.",
 | 
			
		||||
  "chatDirect": "Direct Messages",
 | 
			
		||||
  "back": "返回"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -936,5 +936,7 @@
 | 
			
		||||
  "settingsSoundEffects": "声音效果",
 | 
			
		||||
  "settingsSoundEffectsDescription": "在一些场合下启用声音特效。",
 | 
			
		||||
  "settingsResetMemorizedWindowSize": "重置窗口大小",
 | 
			
		||||
  "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。"
 | 
			
		||||
  "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。",
 | 
			
		||||
  "chatDirect": "私信",
 | 
			
		||||
  "back": "返回"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -396,8 +396,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
    if (!cfg.soundEffects) return;
 | 
			
		||||
 | 
			
		||||
    final player = AudioPlayer(playerId: 'launch-intro-player');
 | 
			
		||||
    await player.play(AssetSource('audio/sfx/launch-intro.mp3'), volume: 0.5);
 | 
			
		||||
    final player = AudioPlayer(playerId: 'launch-done-player');
 | 
			
		||||
    await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8);
 | 
			
		||||
    player.onPlayerComplete.listen((_) {
 | 
			
		||||
      player.dispose();
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ const kNetworkServerStoreKey = 'app_server_url';
 | 
			
		||||
const kAppbarTransparentStoreKey = 'app_bar_transparent';
 | 
			
		||||
const kAppBackgroundStoreKey = 'app_has_background';
 | 
			
		||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
 | 
			
		||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
 | 
			
		||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
 | 
			
		||||
const kAppExpandPostLink = 'app_expand_post_link';
 | 
			
		||||
const kAppExpandChatLink = 'app_expand_chat_link';
 | 
			
		||||
@@ -47,27 +46,17 @@ class ConfigProvider extends ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool drawerIsCollapsed = false;
 | 
			
		||||
  bool drawerIsExpanded = false;
 | 
			
		||||
 | 
			
		||||
  void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
 | 
			
		||||
    bool newDrawerIsCollapsed = false;
 | 
			
		||||
    bool newDrawerIsExpanded = false;
 | 
			
		||||
    if (withMediaQuery) {
 | 
			
		||||
      newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
 | 
			
		||||
      newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
 | 
			
		||||
    } else {
 | 
			
		||||
      final rpb = ResponsiveBreakpoints.of(context);
 | 
			
		||||
      newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
 | 
			
		||||
      newDrawerIsExpanded = rpb.largerThan(TABLET)
 | 
			
		||||
          ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
 | 
			
		||||
              ? false
 | 
			
		||||
              : true
 | 
			
		||||
          : false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (newDrawerIsExpanded != drawerIsExpanded ||
 | 
			
		||||
        newDrawerIsCollapsed != drawerIsCollapsed) {
 | 
			
		||||
      drawerIsExpanded = newDrawerIsExpanded;
 | 
			
		||||
    if (newDrawerIsCollapsed != drawerIsCollapsed) {
 | 
			
		||||
      drawerIsCollapsed = newDrawerIsCollapsed;
 | 
			
		||||
      notifyListeners();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ 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';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
 | 
			
		||||
class AppNavListItem {
 | 
			
		||||
  final String title;
 | 
			
		||||
@@ -135,11 +134,4 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
    _currentIndex = idx;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnRealm? focusedRealm;
 | 
			
		||||
 | 
			
		||||
  void setFocusedRealm(SnRealm? realm) {
 | 
			
		||||
    focusedRealm = realm;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -105,6 +105,7 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
          if (now.day == 1 && now.month == 4) {
 | 
			
		||||
            _notifySoundPlayer.play(
 | 
			
		||||
              AssetSource('audio/notify/metal-pipe.mp3'),
 | 
			
		||||
              volume: 0.6,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
			
		||||
@@ -6,11 +8,14 @@ import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:google_fonts/google_fonts.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_realm.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_select.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
@@ -18,6 +23,7 @@ import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/unauthorized_hint.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
class ChatScreen extends StatefulWidget {
 | 
			
		||||
@@ -35,6 +41,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
  List<SnChannel>? _channels;
 | 
			
		||||
  Map<int, SnChatMessage>? _lastMessages;
 | 
			
		||||
  Map<int, int>? _unreadCounts;
 | 
			
		||||
  Map<int, int>? _unreadCountsGrouped;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchWhatsNew() async {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
@@ -42,19 +49,48 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
    if (resp.data == null) return;
 | 
			
		||||
    final List<dynamic> out = resp.data;
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _unreadCounts = {for (var v in out) v['channel_id']: v['count']};
 | 
			
		||||
      _unreadCounts ??= {};
 | 
			
		||||
      _unreadCountsGrouped ??= {};
 | 
			
		||||
      for (var v in out) {
 | 
			
		||||
        _unreadCounts![v['channel_id']] = v['count'];
 | 
			
		||||
        final channel =
 | 
			
		||||
            _channels?.firstWhereOrNull((ele) => ele.id == v['channel_id']);
 | 
			
		||||
        if (channel != null) {
 | 
			
		||||
          if (channel.realmId != null) {
 | 
			
		||||
            _unreadCountsGrouped![channel.realmId!] ??= 0;
 | 
			
		||||
            _unreadCountsGrouped![channel.realmId!] =
 | 
			
		||||
                (_unreadCountsGrouped![channel.realmId!]! + v['count']).toInt();
 | 
			
		||||
          }
 | 
			
		||||
          if (channel.type == 1) {
 | 
			
		||||
            _unreadCountsGrouped![0] ??= 0;
 | 
			
		||||
            _unreadCountsGrouped![0] =
 | 
			
		||||
                (_unreadCountsGrouped![0]! + v['count']).toInt();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _refreshChannels({bool noRemote = false}) {
 | 
			
		||||
  void _refreshChannels({bool withBoost = false, bool noRemote = false}) {
 | 
			
		||||
    final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!withBoost) {
 | 
			
		||||
      if (!noRemote) {
 | 
			
		||||
        ct.refreshAvailableChannels();
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _channels = ct.availableChannels;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final chan = context.read<ChatChannelProvider>();
 | 
			
		||||
    chan.fetchChannels(noRemote: noRemote).listen((channels) async {
 | 
			
		||||
    chan.fetchChannels(noRemote: true).listen((channels) async {
 | 
			
		||||
      final lastMessages = await chan.getLastMessages(channels);
 | 
			
		||||
      _lastMessages = {for (final val in lastMessages) val.channelId: val};
 | 
			
		||||
      channels.sort((a, b) {
 | 
			
		||||
@@ -96,6 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
      ..onDone(() {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        setState(() => _isBusy = false);
 | 
			
		||||
        _fetchWhatsNew();
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -130,29 +167,48 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _refreshChannels();
 | 
			
		||||
    _fetchWhatsNew();
 | 
			
		||||
    _refreshChannels(withBoost: true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onTapChannel(SnChannel channel) {
 | 
			
		||||
    setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
    GoRouter.of(context).pushReplacementNamed(
 | 
			
		||||
      'chatRoom',
 | 
			
		||||
      pathParameters: {
 | 
			
		||||
        'scope': channel.realm?.alias ?? 'global',
 | 
			
		||||
        'alias': channel.alias,
 | 
			
		||||
      },
 | 
			
		||||
    ).then((value) {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
        _refreshChannels(noRemote: true);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (ResponsiveScaffold.getIsExpand(context)) {
 | 
			
		||||
      GoRouter.of(context).pushReplacementNamed(
 | 
			
		||||
        'chatRoom',
 | 
			
		||||
        pathParameters: {
 | 
			
		||||
          'scope': channel.realm?.alias ?? 'global',
 | 
			
		||||
          'alias': channel.alias,
 | 
			
		||||
        },
 | 
			
		||||
      ).then((value) {
 | 
			
		||||
        if (mounted) {
 | 
			
		||||
          setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
          _refreshChannels(noRemote: true);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      GoRouter.of(context).pushNamed(
 | 
			
		||||
        'chatRoom',
 | 
			
		||||
        pathParameters: {
 | 
			
		||||
          'scope': channel.realm?.alias ?? 'global',
 | 
			
		||||
          'alias': channel.alias,
 | 
			
		||||
        },
 | 
			
		||||
      ).then((value) {
 | 
			
		||||
        if (mounted) {
 | 
			
		||||
          setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
          _refreshChannels(noRemote: true);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnRealm? _focusedRealm;
 | 
			
		||||
  bool _isDirect = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    final rel = context.read<SnRealmProvider>();
 | 
			
		||||
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
@@ -235,34 +291,195 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
              removeTop: true,
 | 
			
		||||
          if (_channels != null && ResponsiveScaffold.getIsExpand(context))
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: RefreshIndicator(
 | 
			
		||||
                onRefresh: () => Future.wait([
 | 
			
		||||
                  Future.sync(() => _refreshChannels()),
 | 
			
		||||
                  _fetchWhatsNew(),
 | 
			
		||||
                ]),
 | 
			
		||||
                child: ListView.builder(
 | 
			
		||||
                  itemCount: _channels?.length ?? 0,
 | 
			
		||||
                  itemBuilder: (context, idx) {
 | 
			
		||||
                    final channel = _channels![idx];
 | 
			
		||||
                    final lastMessage = _lastMessages?[channel.id];
 | 
			
		||||
                onRefresh: () => Future.sync(() => _refreshChannels()),
 | 
			
		||||
                child: Builder(builder: (context) {
 | 
			
		||||
                  final scopeList = ListView(
 | 
			
		||||
                    key: const Key('realm-list-view'),
 | 
			
		||||
                    padding: EdgeInsets.zero,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      ListTile(
 | 
			
		||||
                        minTileHeight: 48,
 | 
			
		||||
                        leading:
 | 
			
		||||
                            const Icon(Symbols.inbox_text).padding(right: 4),
 | 
			
		||||
                        contentPadding: EdgeInsets.only(left: 24, right: 24),
 | 
			
		||||
                        title: Text('chatDirect').tr(),
 | 
			
		||||
                        trailing: Row(
 | 
			
		||||
                          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            if (_unreadCountsGrouped?[0] != null &&
 | 
			
		||||
                                (_unreadCountsGrouped?[0] ?? 0) > 0)
 | 
			
		||||
                              Badge(
 | 
			
		||||
                                label: Text(
 | 
			
		||||
                                  _unreadCountsGrouped![0].toString(),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          setState(() => _isDirect = true);
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                      ...rel.availableRealms.map((ele) {
 | 
			
		||||
                        return ListTile(
 | 
			
		||||
                          minTileHeight: 48,
 | 
			
		||||
                          contentPadding: EdgeInsets.only(left: 20, right: 24),
 | 
			
		||||
                          leading: AccountImage(
 | 
			
		||||
                            content: ele.avatar,
 | 
			
		||||
                            radius: 16,
 | 
			
		||||
                          ),
 | 
			
		||||
                          trailing: Row(
 | 
			
		||||
                            mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                            crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              if (_unreadCountsGrouped?[ele.id] != null &&
 | 
			
		||||
                                  (_unreadCountsGrouped?[ele.id] ?? 0) > 0)
 | 
			
		||||
                                Badge(
 | 
			
		||||
                                  label: Text(
 | 
			
		||||
                                    _unreadCountsGrouped![ele.id].toString(),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                          title: Text(ele.name),
 | 
			
		||||
                          onTap: () {
 | 
			
		||||
                            setState(() => _focusedRealm = ele);
 | 
			
		||||
                          },
 | 
			
		||||
                        );
 | 
			
		||||
                      }),
 | 
			
		||||
                    ],
 | 
			
		||||
                  );
 | 
			
		||||
 | 
			
		||||
                    return _ChatChannelEntry(
 | 
			
		||||
                      channel: channel,
 | 
			
		||||
                      lastMessage: lastMessage,
 | 
			
		||||
                      unreadCount: _unreadCounts?[channel.id],
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        _onTapChannel(channel);
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                  final directChatList = ListView(
 | 
			
		||||
                    key: Key('direct-chat-list-view'),
 | 
			
		||||
                    padding: EdgeInsets.zero,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      ListTile(
 | 
			
		||||
                        minTileHeight: 48,
 | 
			
		||||
                        leading: const Icon(Symbols.arrow_left_alt),
 | 
			
		||||
                        contentPadding: EdgeInsets.only(left: 24),
 | 
			
		||||
                        title: Text('back').tr(),
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          setState(() => _isDirect = false);
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                      const Divider(height: 1),
 | 
			
		||||
                      ..._channels!.where((ele) => ele.type == 1).map(
 | 
			
		||||
                        (ele) {
 | 
			
		||||
                          return _ChatChannelEntry(
 | 
			
		||||
                            channel: ele,
 | 
			
		||||
                            unreadCount: _unreadCounts?[ele.id],
 | 
			
		||||
                            lastMessage: _lastMessages?[ele.id],
 | 
			
		||||
                            isCompact: true,
 | 
			
		||||
                            onTap: () => _onTapChannel(ele),
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                      )
 | 
			
		||||
                    ],
 | 
			
		||||
                  );
 | 
			
		||||
 | 
			
		||||
                  final realmScopedChatList = _focusedRealm == null
 | 
			
		||||
                      ? const SizedBox.shrink()
 | 
			
		||||
                      : ListView(
 | 
			
		||||
                          key: ValueKey(_focusedRealm),
 | 
			
		||||
                          padding: EdgeInsets.zero,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            if (_focusedRealm!.banner != null)
 | 
			
		||||
                              AspectRatio(
 | 
			
		||||
                                aspectRatio: 16 / 9,
 | 
			
		||||
                                child: AutoResizeUniversalImage(
 | 
			
		||||
                                  sn.getAttachmentUrl(
 | 
			
		||||
                                    _focusedRealm!.banner!,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  fit: BoxFit.cover,
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ListTile(
 | 
			
		||||
                              minTileHeight: 48,
 | 
			
		||||
                              tileColor: Theme.of(context)
 | 
			
		||||
                                  .colorScheme
 | 
			
		||||
                                  .surfaceContainer,
 | 
			
		||||
                              leading: AccountImage(
 | 
			
		||||
                                content: _focusedRealm!.avatar,
 | 
			
		||||
                                radius: 16,
 | 
			
		||||
                              ),
 | 
			
		||||
                              contentPadding: EdgeInsets.only(
 | 
			
		||||
                                left: 20,
 | 
			
		||||
                                right: 16,
 | 
			
		||||
                              ),
 | 
			
		||||
                              trailing: IconButton(
 | 
			
		||||
                                icon: const Icon(Symbols.close),
 | 
			
		||||
                                padding: EdgeInsets.zero,
 | 
			
		||||
                                constraints: const BoxConstraints(),
 | 
			
		||||
                                visualDensity: VisualDensity.compact,
 | 
			
		||||
                                onPressed: () {
 | 
			
		||||
                                  setState(() => _focusedRealm = null);
 | 
			
		||||
                                },
 | 
			
		||||
                              ),
 | 
			
		||||
                              title: Text(_focusedRealm!.name),
 | 
			
		||||
                            ),
 | 
			
		||||
                            ...(_channels!
 | 
			
		||||
                                .where(
 | 
			
		||||
                                    (ele) => ele.realmId == _focusedRealm?.id)
 | 
			
		||||
                                .map(
 | 
			
		||||
                              (ele) {
 | 
			
		||||
                                return _ChatChannelEntry(
 | 
			
		||||
                                  channel: ele,
 | 
			
		||||
                                  unreadCount: _unreadCounts?[ele.id],
 | 
			
		||||
                                  lastMessage: _lastMessages?[ele.id],
 | 
			
		||||
                                  onTap: () => _onTapChannel(ele),
 | 
			
		||||
                                  isCompact: true,
 | 
			
		||||
                                );
 | 
			
		||||
                              },
 | 
			
		||||
                            ))
 | 
			
		||||
                          ],
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                  return PageTransitionSwitcher(
 | 
			
		||||
                    duration: const Duration(milliseconds: 300),
 | 
			
		||||
                    transitionBuilder: (Widget child,
 | 
			
		||||
                        Animation<double> primaryAnimation,
 | 
			
		||||
                        Animation<double> secondaryAnimation) {
 | 
			
		||||
                      return SharedAxisTransition(
 | 
			
		||||
                        animation: primaryAnimation,
 | 
			
		||||
                        secondaryAnimation: secondaryAnimation,
 | 
			
		||||
                        fillColor: Colors.transparent,
 | 
			
		||||
                        transitionType: SharedAxisTransitionType.horizontal,
 | 
			
		||||
                        child: child,
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    child: (_focusedRealm == null && !_isDirect)
 | 
			
		||||
                        ? scopeList
 | 
			
		||||
                        : _isDirect
 | 
			
		||||
                            ? directChatList
 | 
			
		||||
                            : realmScopedChatList,
 | 
			
		||||
                  );
 | 
			
		||||
                }),
 | 
			
		||||
              ),
 | 
			
		||||
            )
 | 
			
		||||
          else if (_channels != null)
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: RefreshIndicator(
 | 
			
		||||
                onRefresh: () => Future.sync(() => _refreshChannels()),
 | 
			
		||||
                child: ListView(
 | 
			
		||||
                  key: const Key('chat-list-view'),
 | 
			
		||||
                  padding: EdgeInsets.zero,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    ...(_channels!.map((ele) {
 | 
			
		||||
                      return _ChatChannelEntry(
 | 
			
		||||
                        channel: ele,
 | 
			
		||||
                        unreadCount: _unreadCounts?[ele.id],
 | 
			
		||||
                        lastMessage: _lastMessages?[ele.id],
 | 
			
		||||
                        onTap: () => _onTapChannel(ele),
 | 
			
		||||
                      );
 | 
			
		||||
                    }))
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
@@ -274,11 +491,13 @@ class _ChatChannelEntry extends StatelessWidget {
 | 
			
		||||
  final int? unreadCount;
 | 
			
		||||
  final SnChatMessage? lastMessage;
 | 
			
		||||
  final Function? onTap;
 | 
			
		||||
  final bool isCompact;
 | 
			
		||||
  const _ChatChannelEntry({
 | 
			
		||||
    required this.channel,
 | 
			
		||||
    this.unreadCount,
 | 
			
		||||
    this.lastMessage,
 | 
			
		||||
    this.onTap,
 | 
			
		||||
    this.isCompact = false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -297,6 +516,34 @@ class _ChatChannelEntry extends StatelessWidget {
 | 
			
		||||
        ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
 | 
			
		||||
        : channel.name;
 | 
			
		||||
 | 
			
		||||
    if (isCompact) {
 | 
			
		||||
      return ListTile(
 | 
			
		||||
        minTileHeight: 48,
 | 
			
		||||
        contentPadding:
 | 
			
		||||
            EdgeInsets.only(left: otherMember != null ? 20 : 24, right: 24),
 | 
			
		||||
        leading: otherMember != null
 | 
			
		||||
            ? AccountImage(
 | 
			
		||||
                content: ud.getFromCache(otherMember.accountId)?.avatar,
 | 
			
		||||
                radius: 16,
 | 
			
		||||
              )
 | 
			
		||||
            : const Icon(Symbols.tag),
 | 
			
		||||
        trailing: Row(
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            if (unreadCount != null && (unreadCount ?? 0) > 0)
 | 
			
		||||
              Badge(
 | 
			
		||||
                label: Text(unreadCount.toString()),
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        title: Text(title),
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          onTap?.call();
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ListTile(
 | 
			
		||||
      title: Row(
 | 
			
		||||
        children: [
 | 
			
		||||
@@ -359,7 +606,7 @@ class _ChatChannelEntry extends StatelessWidget {
 | 
			
		||||
        content: otherMember != null
 | 
			
		||||
            ? ud.getFromCache(otherMember.accountId)?.avatar
 | 
			
		||||
            : channel.realm?.avatar,
 | 
			
		||||
        fallbackWidget: const Icon(Symbols.chat, size: 20),
 | 
			
		||||
        fallbackWidget: const Icon(Symbols.tag, size: 20),
 | 
			
		||||
      ),
 | 
			
		||||
      onTap: () => onTap?.call(),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -244,7 +244,8 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
                        GoRouter.of(context).pushNamed('postShuffle');
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(48),
 | 
			
		||||
                    if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
 | 
			
		||||
                      const Gap(48),
 | 
			
		||||
                    Expanded(
 | 
			
		||||
                      child: Center(
 | 
			
		||||
                        child: IconButton(
 | 
			
		||||
 
 | 
			
		||||
@@ -325,20 +325,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
			
		||||
                    setState(() {});
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
                  secondary: const Icon(Symbols.left_panel_close),
 | 
			
		||||
                  title: Text('settingsDrawerPreferCollapse').tr(),
 | 
			
		||||
                  subtitle:
 | 
			
		||||
                      Text('settingsDrawerPreferCollapseDescription').tr(),
 | 
			
		||||
                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
			
		||||
                  value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    _prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
 | 
			
		||||
                    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
                    cfg.calcDrawerSize(context);
 | 
			
		||||
                    setState(() {});
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
                  secondary: const Icon(Symbols.hide),
 | 
			
		||||
                  title: Text('settingsHideBottomNav').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,12 +16,7 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
    final ws = context.watch<WebSocketProvider>();
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    final marginLeft =
 | 
			
		||||
        cfg.drawerIsCollapsed
 | 
			
		||||
            ? 0.0
 | 
			
		||||
            : cfg.drawerIsExpanded
 | 
			
		||||
            ? 304.0
 | 
			
		||||
            : 80.0;
 | 
			
		||||
    final marginLeft = cfg.drawerIsCollapsed ? 0.0 : 80.0;
 | 
			
		||||
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
      listenable: ws,
 | 
			
		||||
@@ -35,41 +30,52 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
            child: GestureDetector(
 | 
			
		||||
              child: Material(
 | 
			
		||||
                elevation: 2,
 | 
			
		||||
                shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
 | 
			
		||||
                shape: const RoundedRectangleBorder(
 | 
			
		||||
                    borderRadius: BorderRadius.all(Radius.circular(16))),
 | 
			
		||||
                color: Theme.of(context).colorScheme.secondaryContainer,
 | 
			
		||||
                child:
 | 
			
		||||
                    ua.isAuthorized
 | 
			
		||||
                        ? Row(
 | 
			
		||||
                          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            if (ws.isBusy)
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverConnecting',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                            else if (!ws.isConnected)
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverDisconnected',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                            else
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverConnected',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            if (ws.isBusy)
 | 
			
		||||
                              const CircularProgressIndicator(
 | 
			
		||||
                                strokeWidth: 2.5,
 | 
			
		||||
                                padding: EdgeInsets.zero,
 | 
			
		||||
                              ).width(12).height(12).padding(horizontal: 4, right: 4)
 | 
			
		||||
                            else if (!ws.isConnected)
 | 
			
		||||
                              const Icon(Symbols.power_off, size: 18)
 | 
			
		||||
                            else
 | 
			
		||||
                              const Icon(Symbols.power, size: 18),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                        : const SizedBox.shrink(),
 | 
			
		||||
              ).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
 | 
			
		||||
                child: ua.isAuthorized
 | 
			
		||||
                    ? Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          if (ws.isBusy)
 | 
			
		||||
                            Text(
 | 
			
		||||
                              'serverConnecting',
 | 
			
		||||
                            ).tr().textColor(Theme.of(context)
 | 
			
		||||
                                .colorScheme
 | 
			
		||||
                                .onSecondaryContainer)
 | 
			
		||||
                          else if (!ws.isConnected)
 | 
			
		||||
                            Text(
 | 
			
		||||
                              'serverDisconnected',
 | 
			
		||||
                            ).tr().textColor(Theme.of(context)
 | 
			
		||||
                                .colorScheme
 | 
			
		||||
                                .onSecondaryContainer)
 | 
			
		||||
                          else
 | 
			
		||||
                            Text(
 | 
			
		||||
                              'serverConnected',
 | 
			
		||||
                            ).tr().textColor(Theme.of(context)
 | 
			
		||||
                                .colorScheme
 | 
			
		||||
                                .onSecondaryContainer),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          if (ws.isBusy)
 | 
			
		||||
                            const CircularProgressIndicator(
 | 
			
		||||
                              strokeWidth: 2.5,
 | 
			
		||||
                              padding: EdgeInsets.zero,
 | 
			
		||||
                            )
 | 
			
		||||
                                .width(12)
 | 
			
		||||
                                .height(12)
 | 
			
		||||
                                .padding(horizontal: 4, right: 4)
 | 
			
		||||
                          else if (!ws.isConnected)
 | 
			
		||||
                            const Icon(Symbols.power_off, size: 18)
 | 
			
		||||
                          else
 | 
			
		||||
                            const Icon(Symbols.power, size: 18),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
              )
 | 
			
		||||
                  .opacity(show ? 1 : 0, animate: true)
 | 
			
		||||
                  .animate(const Duration(milliseconds: 300), Curves.easeInOut),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                if (!ws.isConnected && !ws.isBusy) {
 | 
			
		||||
                  ws.connect();
 | 
			
		||||
 
 | 
			
		||||
@@ -26,9 +26,7 @@ class ContextMenuArea extends StatelessWidget {
 | 
			
		||||
        final cfg = context.read<ConfigProvider>();
 | 
			
		||||
        if (!cfg.drawerIsCollapsed) {
 | 
			
		||||
          // Leave padding for side navigation
 | 
			
		||||
          mousePosition = cfg.drawerIsExpanded
 | 
			
		||||
              ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
 | 
			
		||||
              : mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
 | 
			
		||||
          mousePosition = mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      child: GestureDetector(
 | 
			
		||||
@@ -40,7 +38,8 @@ class ContextMenuArea extends StatelessWidget {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _showMenu(BuildContext context, Offset mousePosition) async {
 | 
			
		||||
    final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
 | 
			
		||||
    final menu =
 | 
			
		||||
        contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
 | 
			
		||||
    final value = await showContextMenu(context, contextMenu: menu);
 | 
			
		||||
    onItemSelected?.call(value);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
@@ -11,14 +10,9 @@ import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/navigation.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_realm.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:surface/widgets/version_label.dart';
 | 
			
		||||
 | 
			
		||||
class AppNavigationDrawer extends StatefulWidget {
 | 
			
		||||
@@ -45,27 +39,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
 | 
			
		||||
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
      listenable: nav,
 | 
			
		||||
      builder: (context, _) {
 | 
			
		||||
        return Drawer(
 | 
			
		||||
          elevation: widget.elevation,
 | 
			
		||||
          backgroundColor: backgroundColor,
 | 
			
		||||
          shape: const RoundedRectangleBorder(
 | 
			
		||||
              borderRadius: BorderRadius.all(Radius.circular(0))),
 | 
			
		||||
          child: Column(
 | 
			
		||||
            mainAxisSize: MainAxisSize.max,
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              if (!kIsWeb &&
 | 
			
		||||
                  (Platform.isWindows ||
 | 
			
		||||
                      Platform.isLinux ||
 | 
			
		||||
                      Platform.isMacOS) &&
 | 
			
		||||
                  !cfg.drawerIsExpanded)
 | 
			
		||||
                  (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
 | 
			
		||||
                Container(
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                    border: Border(
 | 
			
		||||
@@ -78,42 +63,36 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
                  child: WindowTitleBarBox(),
 | 
			
		||||
                ),
 | 
			
		||||
              Gap(MediaQuery.of(context).padding.top),
 | 
			
		||||
              Expanded(
 | 
			
		||||
                child: _DrawerContentList(),
 | 
			
		||||
              Column(
 | 
			
		||||
                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                children: [
 | 
			
		||||
                  Text('Solar Network').bold(),
 | 
			
		||||
                  AppVersionLabel(),
 | 
			
		||||
                ],
 | 
			
		||||
              ).padding(
 | 
			
		||||
                horizontal: 32,
 | 
			
		||||
                vertical: 12,
 | 
			
		||||
              ),
 | 
			
		||||
              Expanded(
 | 
			
		||||
                child: ListView(
 | 
			
		||||
                  padding: EdgeInsets.zero,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    ...nav.destinations.mapIndexed((idx, ele) {
 | 
			
		||||
                      return ListTile(
 | 
			
		||||
                        leading: ele.icon,
 | 
			
		||||
                        title: Text(ele.label).tr(),
 | 
			
		||||
                        contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                        selected: nav.currentIndex == idx,
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          GoRouter.of(context).pushNamed(ele.screen);
 | 
			
		||||
                          nav.setIndex(idx);
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                    })
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              Row(
 | 
			
		||||
                spacing: 8,
 | 
			
		||||
                children:
 | 
			
		||||
                    nav.destinations.where((ele) => ele.isPinned).mapIndexed(
 | 
			
		||||
                  (idx, ele) {
 | 
			
		||||
                    return Expanded(
 | 
			
		||||
                      child: Tooltip(
 | 
			
		||||
                        message: ele.label.tr(),
 | 
			
		||||
                        child: IconButton(
 | 
			
		||||
                          icon: ele.icon,
 | 
			
		||||
                          color: nav.currentIndex == idx
 | 
			
		||||
                              ? Theme.of(context).colorScheme.onPrimaryContainer
 | 
			
		||||
                              : Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          style: ButtonStyle(
 | 
			
		||||
                            backgroundColor: WidgetStatePropertyAll(
 | 
			
		||||
                              nav.currentIndex == idx
 | 
			
		||||
                                  ? Theme.of(context)
 | 
			
		||||
                                      .colorScheme
 | 
			
		||||
                                      .primaryContainer
 | 
			
		||||
                                  : Colors.transparent,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            GoRouter.of(context).goNamed(ele.screen);
 | 
			
		||||
                            Scaffold.of(context).closeDrawer();
 | 
			
		||||
                            nav.setIndex(idx);
 | 
			
		||||
                          },
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ).toList(),
 | 
			
		||||
              ).padding(horizontal: 16, bottom: 8),
 | 
			
		||||
              Align(
 | 
			
		||||
                alignment: Alignment.bottomCenter,
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
@@ -167,163 +146,3 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
  const _DrawerContentList();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
    final rel = context.watch<SnRealmProvider>();
 | 
			
		||||
 | 
			
		||||
    return PageTransitionSwitcher(
 | 
			
		||||
      duration: const Duration(milliseconds: 300),
 | 
			
		||||
      transitionBuilder: (Widget child, Animation<double> primaryAnimation,
 | 
			
		||||
          Animation<double> secondaryAnimation) {
 | 
			
		||||
        return SharedAxisTransition(
 | 
			
		||||
          animation: primaryAnimation,
 | 
			
		||||
          secondaryAnimation: secondaryAnimation,
 | 
			
		||||
          fillColor: Colors.transparent,
 | 
			
		||||
          transitionType: SharedAxisTransitionType.horizontal,
 | 
			
		||||
          child: child,
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
      child: nav.focusedRealm == null
 | 
			
		||||
          ? ListView(
 | 
			
		||||
              key: const Key('realm-list-view'),
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              children: [
 | 
			
		||||
                Column(
 | 
			
		||||
                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text('Solar Network').bold(),
 | 
			
		||||
                    AppVersionLabel(),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(
 | 
			
		||||
                  horizontal: 32,
 | 
			
		||||
                  vertical: 12,
 | 
			
		||||
                ),
 | 
			
		||||
                ...rel.availableRealms.map((ele) {
 | 
			
		||||
                  return ListTile(
 | 
			
		||||
                    minTileHeight: 48,
 | 
			
		||||
                    contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                    leading: AccountImage(
 | 
			
		||||
                      content: ele.avatar,
 | 
			
		||||
                      radius: 16,
 | 
			
		||||
                    ),
 | 
			
		||||
                    title: Text(ele.name),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      nav.setFocusedRealm(ele);
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                }),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(left: 28, right: 16),
 | 
			
		||||
                  leading: const Icon(Symbols.globe).padding(right: 4),
 | 
			
		||||
                  title: Text('screenRealmDiscovery').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed('realmDiscovery');
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            )
 | 
			
		||||
          : ListView(
 | 
			
		||||
              key: ValueKey(nav.focusedRealm),
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              children: [
 | 
			
		||||
                if (nav.focusedRealm!.banner != null)
 | 
			
		||||
                  AspectRatio(
 | 
			
		||||
                    aspectRatio: 16 / 9,
 | 
			
		||||
                    child: AutoResizeUniversalImage(
 | 
			
		||||
                      sn.getAttachmentUrl(
 | 
			
		||||
                        nav.focusedRealm!.banner!,
 | 
			
		||||
                      ),
 | 
			
		||||
                      fit: BoxFit.cover,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  tileColor: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(
 | 
			
		||||
                    left: 24,
 | 
			
		||||
                    right: 16,
 | 
			
		||||
                  ),
 | 
			
		||||
                  leading: AccountImage(
 | 
			
		||||
                    content: nav.focusedRealm!.avatar,
 | 
			
		||||
                    radius: 16,
 | 
			
		||||
                  ),
 | 
			
		||||
                  trailing: IconButton(
 | 
			
		||||
                    icon: const Icon(Symbols.close),
 | 
			
		||||
                    padding: EdgeInsets.zero,
 | 
			
		||||
                    constraints: const BoxConstraints(),
 | 
			
		||||
                    visualDensity: VisualDensity.compact,
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      nav.setFocusedRealm(null);
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: Text(nav.focusedRealm!.name),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).goNamed(
 | 
			
		||||
                      'realmDetail',
 | 
			
		||||
                      pathParameters: {
 | 
			
		||||
                        'alias': nav.focusedRealm!.alias,
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(
 | 
			
		||||
                    left: 28,
 | 
			
		||||
                    right: 8,
 | 
			
		||||
                  ),
 | 
			
		||||
                  leading: const Icon(Symbols.globe),
 | 
			
		||||
                  title: Text('community').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).goNamed(
 | 
			
		||||
                      'realmCommunity',
 | 
			
		||||
                      pathParameters: {
 | 
			
		||||
                        'alias': nav.focusedRealm!.alias,
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                if (ct.availableChannels
 | 
			
		||||
                    .where((ele) => ele.realmId == nav.focusedRealm?.id)
 | 
			
		||||
                    .isNotEmpty)
 | 
			
		||||
                  const Divider(height: 1),
 | 
			
		||||
                ...(ct.availableChannels
 | 
			
		||||
                    .where((ele) => ele.realmId == nav.focusedRealm?.id)
 | 
			
		||||
                    .map((ele) {
 | 
			
		||||
                  return ListTile(
 | 
			
		||||
                    minTileHeight: 48,
 | 
			
		||||
                    contentPadding: EdgeInsets.only(
 | 
			
		||||
                      left: 28,
 | 
			
		||||
                      right: 8,
 | 
			
		||||
                    ),
 | 
			
		||||
                    leading: const Icon(Symbols.tag),
 | 
			
		||||
                    title: Text(ele.name),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      GoRouter.of(context).goNamed(
 | 
			
		||||
                        'chatRoom',
 | 
			
		||||
                        pathParameters: {
 | 
			
		||||
                          'scope': ele.realm?.alias ?? 'global',
 | 
			
		||||
                          'alias': ele.alias,
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                      Scaffold.of(context).closeDrawer();
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                }))
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import 'package:surface/providers/navigation.dart';
 | 
			
		||||
import 'package:surface/widgets/connection_indicator.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';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
 | 
			
		||||
import 'package:surface/widgets/notify_indicator.dart';
 | 
			
		||||
 | 
			
		||||
@@ -221,6 +222,7 @@ class AppRootScaffold extends StatelessWidget {
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      drawerEdgeDragWidth: isPopable ? 0 : null,
 | 
			
		||||
      drawer: isCollapseDrawer ? const AppNavigationDrawer() : null,
 | 
			
		||||
      bottomNavigationBar:
 | 
			
		||||
          isShowBottomNavigation ? AppBottomNavigationBar() : null,
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user