Desktop chat list

🍱 Update launch sfx
This commit is contained in:
LittleSheep 2025-03-28 00:52:19 +08:00
parent 6bb9c21759
commit 7c2b8de931
7 changed files with 275 additions and 214 deletions

Binary file not shown.

View File

@ -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": "返回"
}

View File

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

View File

@ -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();
});

View File

@ -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();
}
}

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: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) {
@ -130,29 +166,49 @@ class _ChatScreenState extends State<ChatScreen> {
@override
void initState() {
super.initState();
_refreshChannels();
_refreshChannels(withBoost: true);
_fetchWhatsNew();
}
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,178 @@ class _ChatScreenState extends State<ChatScreen> {
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
if (_channels != null)
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];
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,
);
}),
),
),
),
],
),
);
@ -274,11 +474,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 +499,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 +589,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(),
);

View File

@ -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,13 +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/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 {
@ -151,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();
},
);
}))
],
),
);
}
}