Compare commits

..

No commits in common. "908f0cb59ec0546d05e11b09372e24908e01e2c3" and "a1c4e5eca04cef112f096a66953fbafcab12457f" have entirely different histories.

14 changed files with 334 additions and 380 deletions

Binary file not shown.

View File

@ -939,7 +939,5 @@
"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,7 +936,5 @@
"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-done-player'); final player = AudioPlayer(playerId: 'launch-intro-player');
await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8); await player.play(AssetSource('audio/sfx/launch-intro.mp3'), volume: 0.5);
player.onPlayerComplete.listen((_) { player.onPlayerComplete.listen((_) {
player.dispose(); player.dispose();
}); });

View File

@ -13,6 +13,7 @@ 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';
@ -46,17 +47,27 @@ 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 (newDrawerIsCollapsed != drawerIsCollapsed) { if (newDrawerIsExpanded != drawerIsExpanded ||
newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed; drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners(); notifyListeners();
} }

View File

@ -4,6 +4,7 @@ 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;
@ -134,4 +135,11 @@ class NavigationProvider extends ChangeNotifier {
_currentIndex = idx; _currentIndex = idx;
notifyListeners(); notifyListeners();
} }
SnRealm? focusedRealm;
void setFocusedRealm(SnRealm? realm) {
focusedRealm = realm;
notifyListeners();
}
} }

View File

@ -105,7 +105,6 @@ 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,5 +1,3 @@
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';
@ -8,14 +6,11 @@ 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';
@ -23,7 +18,6 @@ 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 {
@ -41,7 +35,6 @@ 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>();
@ -49,48 +42,19 @@ 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 ??= {}; _unreadCounts = {for (var v in out) v['channel_id']: v['count']};
_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 withBoost = false, bool noRemote = false}) { void _refreshChannels({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: true).listen((channels) async { chan.fetchChannels(noRemote: noRemote).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) {
@ -132,7 +96,6 @@ class _ChatScreenState extends State<ChatScreen> {
..onDone(() { ..onDone(() {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = false); setState(() => _isBusy = false);
_fetchWhatsNew();
}); });
} }
@ -167,12 +130,12 @@ class _ChatScreenState extends State<ChatScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_refreshChannels(withBoost: true); _refreshChannels();
_fetchWhatsNew();
} }
void _onTapChannel(SnChannel channel) { void _onTapChannel(SnChannel channel) {
setState(() => _unreadCounts?[channel.id] = 0); setState(() => _unreadCounts?[channel.id] = 0);
if (ResponsiveScaffold.getIsExpand(context)) {
GoRouter.of(context).pushReplacementNamed( GoRouter.of(context).pushReplacementNamed(
'chatRoom', 'chatRoom',
pathParameters: { pathParameters: {
@ -185,30 +148,11 @@ class _ChatScreenState extends State<ChatScreen> {
_refreshChannels(noRemote: true); _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(
@ -291,192 +235,31 @@ class _ChatScreenState extends State<ChatScreen> {
body: Column( body: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
if (_channels != null && ResponsiveScaffold.getIsExpand(context))
Expanded( Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => Future.sync(() => _refreshChannels()), onRefresh: () => Future.wait([
child: Builder(builder: (context) { Future.sync(() => _refreshChannels()),
final scopeList = ListView( _fetchWhatsNew(),
key: const Key('realm-list-view'), ]),
padding: EdgeInsets.zero, child: ListView.builder(
children: [ itemCount: _channels?.length ?? 0,
ListTile( itemBuilder: (context, idx) {
minTileHeight: 48, final channel = _channels![idx];
leading: final lastMessage = _lastMessages?[channel.id];
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);
},
);
}),
],
);
final directChatList = ListView( return _ChatChannelEntry(
key: Key('direct-chat-list-view'), channel: channel,
padding: EdgeInsets.zero, lastMessage: lastMessage,
children: [ unreadCount: _unreadCounts?[channel.id],
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.arrow_left_alt),
contentPadding: EdgeInsets.only(left: 24),
title: Text('back').tr(),
onTap: () { onTap: () {
setState(() => _isDirect = false); _onTapChannel(channel);
},
);
}, },
), ),
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),
);
}))
],
), ),
), ),
), ),
@ -491,13 +274,11 @@ 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
@ -516,34 +297,6 @@ 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: [
@ -606,7 +359,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.tag, size: 20), fallbackWidget: const Icon(Symbols.chat, size: 20),
), ),
onTap: () => onTap?.call(), onTap: () => onTap?.call(),
); );

View File

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

View File

@ -325,6 +325,20 @@ 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,7 +16,12 @@ 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 = cfg.drawerIsCollapsed ? 0.0 : 80.0; final marginLeft =
cfg.drawerIsCollapsed
? 0.0
: cfg.drawerIsExpanded
? 304.0
: 80.0;
return ListenableBuilder( return ListenableBuilder(
listenable: ws, listenable: ws,
@ -30,10 +35,10 @@ class ConnectionIndicator extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
child: Material( child: Material(
elevation: 2, elevation: 2,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
borderRadius: BorderRadius.all(Radius.circular(16))),
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context).colorScheme.secondaryContainer,
child: ua.isAuthorized child:
ua.isAuthorized
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -42,30 +47,21 @@ class ConnectionIndicator extends StatelessWidget {
if (ws.isBusy) if (ws.isBusy)
Text( Text(
'serverConnecting', 'serverConnecting',
).tr().textColor(Theme.of(context) ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
.colorScheme
.onSecondaryContainer)
else if (!ws.isConnected) else if (!ws.isConnected)
Text( Text(
'serverDisconnected', 'serverDisconnected',
).tr().textColor(Theme.of(context) ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
.colorScheme
.onSecondaryContainer)
else else
Text( Text(
'serverConnected', 'serverConnected',
).tr().textColor(Theme.of(context) ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
.colorScheme
.onSecondaryContainer),
const Gap(8), const Gap(8),
if (ws.isBusy) if (ws.isBusy)
const CircularProgressIndicator( const CircularProgressIndicator(
strokeWidth: 2.5, strokeWidth: 2.5,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
) ).width(12).height(12).padding(horizontal: 4, right: 4)
.width(12)
.height(12)
.padding(horizontal: 4, right: 4)
else if (!ws.isConnected) else if (!ws.isConnected)
const Icon(Symbols.power_off, size: 18) const Icon(Symbols.power_off, size: 18)
else else
@ -73,9 +69,7 @@ class ConnectionIndicator extends StatelessWidget {
], ],
).padding(horizontal: 8, vertical: 4) ).padding(horizontal: 8, vertical: 4)
: const SizedBox.shrink(), : const SizedBox.shrink(),
) ).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
.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,7 +26,9 @@ 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 = mousePosition.copyWith(dx: mousePosition.dx - 80 * 2); mousePosition = cfg.drawerIsExpanded
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
} }
}, },
child: GestureDetector( child: GestureDetector(
@ -38,8 +40,7 @@ class ContextMenuArea extends StatelessWidget {
} }
void _showMenu(BuildContext context, Offset mousePosition) async { void _showMenu(BuildContext context, Offset mousePosition) async {
final menu = final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
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,5 +1,6 @@
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';
@ -10,9 +11,14 @@ 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 {
@ -39,18 +45,27 @@ 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.isLinux || Platform.isMacOS)) (Platform.isWindows ||
Platform.isLinux ||
Platform.isMacOS) &&
!cfg.drawerIsExpanded)
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
@ -63,36 +78,42 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
child: WindowTitleBarBox(), child: WindowTitleBarBox(),
), ),
Gap(MediaQuery.of(context).padding.top), Gap(MediaQuery.of(context).padding.top),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
AppVersionLabel(),
],
).padding(
horizontal: 32,
vertical: 12,
),
Expanded( Expanded(
child: ListView( child: _DrawerContentList(),
padding: EdgeInsets.zero, ),
children: [ Row(
...nav.destinations.mapIndexed((idx, ele) { spacing: 8,
return ListTile( children:
leading: ele.icon, nav.destinations.where((ele) => ele.isPinned).mapIndexed(
title: Text(ele.label).tr(), (idx, ele) {
contentPadding: EdgeInsets.symmetric(horizontal: 24), return Expanded(
selected: nav.currentIndex == idx, child: Tooltip(
onTap: () { message: ele.label.tr(),
GoRouter.of(context).pushNamed(ele.screen); 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); nav.setIndex(idx);
}, },
),
),
); );
}) },
], ).toList(),
), ).padding(horizontal: 16, bottom: 8),
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ListTile( child: ListTile(
@ -146,3 +167,163 @@ 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,7 +13,6 @@ 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';
@ -222,7 +221,6 @@ 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,
); );