Compare commits

..

4 Commits

Author SHA1 Message Date
908f0cb59e 🐛 Fix some issues 2025-03-28 00:54:51 +08:00
7c2b8de931 Desktop chat list
🍱 Update launch sfx
2025-03-28 00:52:19 +08:00
6bb9c21759 Rollback drawer style on mobile
🗑️ Remove drawer prefer collapse & expand
2025-03-28 00:00:39 +08:00
8f2fc55608 User ear healthy 2025-03-27 23:52:37 +08:00
14 changed files with 381 additions and 335 deletions

Binary file not shown.

View File

@ -939,5 +939,7 @@
"settingsSoundEffects": "Sound Effects", "settingsSoundEffects": "Sound Effects",
"settingsSoundEffectsDescription": "Enable the sound effects around the app.", "settingsSoundEffectsDescription": "Enable the sound effects around the app.",
"settingsResetMemorizedWindowSize": "Reset Window Size", "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": "返回"
} }

View File

@ -936,5 +936,7 @@
"settingsSoundEffects": "声音效果", "settingsSoundEffects": "声音效果",
"settingsSoundEffectsDescription": "在一些场合下启用声音特效。", "settingsSoundEffectsDescription": "在一些场合下启用声音特效。",
"settingsResetMemorizedWindowSize": "重置窗口大小", "settingsResetMemorizedWindowSize": "重置窗口大小",
"settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。" "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。",
"chatDirect": "私信",
"back": "返回"
} }

View File

@ -396,8 +396,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
if (!cfg.soundEffects) return; if (!cfg.soundEffects) return;
final player = AudioPlayer(playerId: 'launch-intro-player'); final player = AudioPlayer(playerId: 'launch-done-player');
await player.play(AssetSource('audio/sfx/launch-intro.mp3'), volume: 0.5); await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8);
player.onPlayerComplete.listen((_) { player.onPlayerComplete.listen((_) {
player.dispose(); player.dispose();
}); });

View File

@ -13,7 +13,6 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background'; const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme'; const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic'; const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link'; const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link'; const kAppExpandChatLink = 'app_expand_chat_link';
@ -47,27 +46,17 @@ class ConfigProvider extends ChangeNotifier {
} }
bool drawerIsCollapsed = false; bool drawerIsCollapsed = false;
bool drawerIsExpanded = false;
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) { void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
bool newDrawerIsCollapsed = false; bool newDrawerIsCollapsed = false;
bool newDrawerIsExpanded = false;
if (withMediaQuery) { if (withMediaQuery) {
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600; newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
} else { } else {
final rpb = ResponsiveBreakpoints.of(context); final rpb = ResponsiveBreakpoints.of(context);
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
newDrawerIsExpanded = rpb.largerThan(TABLET)
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
? false
: true
: false;
} }
if (newDrawerIsExpanded != drawerIsExpanded || if (newDrawerIsCollapsed != drawerIsCollapsed) {
newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed; drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners(); notifyListeners();
} }

View File

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/types/realm.dart';
class AppNavListItem { class AppNavListItem {
final String title; final String title;
@ -135,11 +134,4 @@ class NavigationProvider extends ChangeNotifier {
_currentIndex = idx; _currentIndex = idx;
notifyListeners(); notifyListeners();
} }
SnRealm? focusedRealm;
void setFocusedRealm(SnRealm? realm) {
focusedRealm = realm;
notifyListeners();
}
} }

View File

@ -105,6 +105,7 @@ class NotificationProvider extends ChangeNotifier {
if (now.day == 1 && now.month == 4) { if (now.day == 1 && now.month == 4) {
_notifySoundPlayer.play( _notifySoundPlayer.play(
AssetSource('audio/notify/metal-pipe.mp3'), AssetSource('audio/notify/metal-pipe.mp3'),
volume: 0.6,
); );
} }
} }

View File

@ -1,3 +1,5 @@
import 'package:animations/animations.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.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:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/sn_network.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/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.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_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/app_bar_leading.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/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
@ -35,6 +41,7 @@ class _ChatScreenState extends State<ChatScreen> {
List<SnChannel>? _channels; List<SnChannel>? _channels;
Map<int, SnChatMessage>? _lastMessages; Map<int, SnChatMessage>? _lastMessages;
Map<int, int>? _unreadCounts; Map<int, int>? _unreadCounts;
Map<int, int>? _unreadCountsGrouped;
Future<void> _fetchWhatsNew() async { Future<void> _fetchWhatsNew() async {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
@ -42,19 +49,48 @@ class _ChatScreenState extends State<ChatScreen> {
if (resp.data == null) return; if (resp.data == null) return;
final List<dynamic> out = resp.data; final List<dynamic> out = resp.data;
setState(() { 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>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
setState(() => _isBusy = false); setState(() => _isBusy = false);
return; return;
} }
if (!withBoost) {
if (!noRemote) {
ct.refreshAvailableChannels();
}
} else {
setState(() {
_channels = ct.availableChannels;
});
}
final chan = context.read<ChatChannelProvider>(); 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); final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val}; _lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) { channels.sort((a, b) {
@ -96,6 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
..onDone(() { ..onDone(() {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = false); setState(() => _isBusy = false);
_fetchWhatsNew();
}); });
} }
@ -130,29 +167,48 @@ class _ChatScreenState extends State<ChatScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_refreshChannels(); _refreshChannels(withBoost: true);
_fetchWhatsNew();
} }
void _onTapChannel(SnChannel channel) { void _onTapChannel(SnChannel channel) {
setState(() => _unreadCounts?[channel.id] = 0); setState(() => _unreadCounts?[channel.id] = 0);
GoRouter.of(context).pushReplacementNamed( if (ResponsiveScaffold.getIsExpand(context)) {
'chatRoom', GoRouter.of(context).pushReplacementNamed(
pathParameters: { 'chatRoom',
'scope': channel.realm?.alias ?? 'global', pathParameters: {
'alias': channel.alias, 'scope': channel.realm?.alias ?? 'global',
}, 'alias': channel.alias,
).then((value) { },
if (mounted) { ).then((value) {
setState(() => _unreadCounts?[channel.id] = 0); if (mounted) {
_refreshChannels(noRemote: true); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final sn = context.read<SnNetworkProvider>();
final rel = context.read<SnRealmProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
@ -235,34 +291,195 @@ class _ChatScreenState extends State<ChatScreen> {
body: Column( body: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( if (_channels != null && ResponsiveScaffold.getIsExpand(context))
child: MediaQuery.removePadding( Expanded(
context: context,
removeTop: true,
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => Future.wait([ onRefresh: () => Future.sync(() => _refreshChannels()),
Future.sync(() => _refreshChannels()), child: Builder(builder: (context) {
_fetchWhatsNew(), final scopeList = ListView(
]), key: const Key('realm-list-view'),
child: ListView.builder( padding: EdgeInsets.zero,
itemCount: _channels?.length ?? 0, children: [
itemBuilder: (context, idx) { ListTile(
final channel = _channels![idx]; minTileHeight: 48,
final lastMessage = _lastMessages?[channel.id]; 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( final directChatList = ListView(
channel: channel, key: Key('direct-chat-list-view'),
lastMessage: lastMessage, padding: EdgeInsets.zero,
unreadCount: _unreadCounts?[channel.id], children: [
onTap: () { ListTile(
_onTapChannel(channel); 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 int? unreadCount;
final SnChatMessage? lastMessage; final SnChatMessage? lastMessage;
final Function? onTap; final Function? onTap;
final bool isCompact;
const _ChatChannelEntry({ const _ChatChannelEntry({
required this.channel, required this.channel,
this.unreadCount, this.unreadCount,
this.lastMessage, this.lastMessage,
this.onTap, this.onTap,
this.isCompact = false,
}); });
@override @override
@ -297,6 +516,34 @@ class _ChatChannelEntry extends StatelessWidget {
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
: 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( return ListTile(
title: Row( title: Row(
children: [ children: [
@ -359,7 +606,7 @@ class _ChatChannelEntry extends StatelessWidget {
content: otherMember != null content: otherMember != null
? ud.getFromCache(otherMember.accountId)?.avatar ? ud.getFromCache(otherMember.accountId)?.avatar
: channel.realm?.avatar, : channel.realm?.avatar,
fallbackWidget: const Icon(Symbols.chat, size: 20), fallbackWidget: const Icon(Symbols.tag, size: 20),
), ),
onTap: () => onTap?.call(), onTap: () => onTap?.call(),
); );

View File

@ -244,7 +244,8 @@ class _ExploreScreenState extends State<ExploreScreen>
GoRouter.of(context).pushNamed('postShuffle'); GoRouter.of(context).pushNamed('postShuffle');
}, },
), ),
const Gap(48), if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
const Gap(48),
Expanded( Expanded(
child: Center( child: Center(
child: IconButton( child: IconButton(

View File

@ -325,20 +325,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {}); 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( CheckboxListTile(
secondary: const Icon(Symbols.hide), secondary: const Icon(Symbols.hide),
title: Text('settingsHideBottomNav').tr(), title: Text('settingsHideBottomNav').tr(),

View File

@ -16,12 +16,7 @@ class ConnectionIndicator extends StatelessWidget {
final ws = context.watch<WebSocketProvider>(); final ws = context.watch<WebSocketProvider>();
final cfg = context.watch<ConfigProvider>(); final cfg = context.watch<ConfigProvider>();
final marginLeft = final marginLeft = cfg.drawerIsCollapsed ? 0.0 : 80.0;
cfg.drawerIsCollapsed
? 0.0
: cfg.drawerIsExpanded
? 304.0
: 80.0;
return ListenableBuilder( return ListenableBuilder(
listenable: ws, listenable: ws,
@ -35,41 +30,52 @@ class ConnectionIndicator extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
child: Material( child: Material(
elevation: 2, 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, color: Theme.of(context).colorScheme.secondaryContainer,
child: child: ua.isAuthorized
ua.isAuthorized ? Row(
? Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ if (ws.isBusy)
if (ws.isBusy) Text(
Text( 'serverConnecting',
'serverConnecting', ).tr().textColor(Theme.of(context)
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) .colorScheme
else if (!ws.isConnected) .onSecondaryContainer)
Text( else if (!ws.isConnected)
'serverDisconnected', Text(
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) 'serverDisconnected',
else ).tr().textColor(Theme.of(context)
Text( .colorScheme
'serverConnected', .onSecondaryContainer)
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), else
const Gap(8), Text(
if (ws.isBusy) 'serverConnected',
const CircularProgressIndicator( ).tr().textColor(Theme.of(context)
strokeWidth: 2.5, .colorScheme
padding: EdgeInsets.zero, .onSecondaryContainer),
).width(12).height(12).padding(horizontal: 4, right: 4) const Gap(8),
else if (!ws.isConnected) if (ws.isBusy)
const Icon(Symbols.power_off, size: 18) const CircularProgressIndicator(
else strokeWidth: 2.5,
const Icon(Symbols.power, size: 18), padding: EdgeInsets.zero,
], )
).padding(horizontal: 8, vertical: 4) .width(12)
: const SizedBox.shrink(), .height(12)
).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut), .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: () { onTap: () {
if (!ws.isConnected && !ws.isBusy) { if (!ws.isConnected && !ws.isBusy) {
ws.connect(); ws.connect();

View File

@ -26,9 +26,7 @@ class ContextMenuArea extends StatelessWidget {
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
if (!cfg.drawerIsCollapsed) { if (!cfg.drawerIsCollapsed) {
// Leave padding for side navigation // Leave padding for side navigation
mousePosition = cfg.drawerIsExpanded mousePosition = mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
} }
}, },
child: GestureDetector( child: GestureDetector(
@ -40,7 +38,8 @@ class ContextMenuArea extends StatelessWidget {
} }
void _showMenu(BuildContext context, Offset mousePosition) async { 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); final value = await showContextMenu(context, contextMenu: menu);
onItemSelected?.call(value); onItemSelected?.call(value);
} }

View File

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:animations/animations.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.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:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.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/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/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawer extends StatefulWidget {
@ -45,27 +39,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
final cfg = context.watch<ConfigProvider>();
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
return Drawer( return Drawer(
elevation: widget.elevation, elevation: widget.elevation,
backgroundColor: backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0))),
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!kIsWeb && if (!kIsWeb &&
(Platform.isWindows || (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
Platform.isLinux ||
Platform.isMacOS) &&
!cfg.drawerIsExpanded)
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
@ -78,42 +63,36 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
child: WindowTitleBarBox(), child: WindowTitleBarBox(),
), ),
Gap(MediaQuery.of(context).padding.top), Gap(MediaQuery.of(context).padding.top),
Expanded( Column(
child: _DrawerContentList(), 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( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ListTile( 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();
},
);
}))
],
),
);
}
}

View File

@ -13,6 +13,7 @@ import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart'; import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.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/navigation/app_rail_navigation.dart';
import 'package:surface/widgets/notify_indicator.dart'; import 'package:surface/widgets/notify_indicator.dart';
@ -221,6 +222,7 @@ class AppRootScaffold extends StatelessWidget {
], ],
), ),
drawerEdgeDragWidth: isPopable ? 0 : null, drawerEdgeDragWidth: isPopable ? 0 : null,
drawer: isCollapseDrawer ? const AppNavigationDrawer() : null,
bottomNavigationBar: bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null, isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );