Compare commits

...

13 Commits

Author SHA1 Message Date
9910fc7a92 Channel content auto refresh after long time background activity 2024-08-10 00:43:55 +08:00
2356eac118 Better side navigation bar 2024-08-09 22:59:24 +08:00
0135b8d838 Better screenshare 2024-08-09 22:40:05 +08:00
8ec33ccbf4 🚨 Fix CarouselController import issue 2024-08-07 19:21:01 +08:00
d267316a35 💄 Better emotes 2024-08-07 19:11:52 +08:00
138da60e55 🚸 Prevent user from sending empty message 2024-08-07 19:02:49 +08:00
4562c2f991 🐛 Fix able send space message 2024-08-07 18:31:26 +08:00
8009f4ca9b 💄 Better sidebar navigation 2024-08-07 18:24:16 +08:00
54dee9702b 🐛 Fix attachments max width 2024-08-07 14:34:41 +08:00
94385564bd 🐛 Fix dupe attachment notification 2024-08-07 14:27:23 +08:00
0b2309816f 🐛 Fix desktop panic when download things 2024-08-07 13:50:50 +08:00
8283272a3b 🗑️ Fix mis-import 2024-08-07 01:49:03 +08:00
eb02a47e9a 💄 Fixes and improvements 2024-08-07 01:47:53 +08:00
20 changed files with 414 additions and 294 deletions

View File

@@ -52,6 +52,17 @@ class Realm {
'is_community': isCommunity, 'is_community': isCommunity,
'account_id': accountId, 'account_id': accountId,
}; };
@override
bool operator ==(Object other) {
if (other is Realm) {
return other.id == id;
}
return false;
}
@override
int get hashCode => id;
} }
class RealmMember { class RealmMember {

View File

@@ -1,4 +1,3 @@
import 'package:get/get.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@@ -32,7 +31,7 @@ class Sticker {
required this.account, required this.account,
}); });
String get textPlaceholder => '${pack?.prefix}$alias'.camelCase!; String get textPlaceholder => '${pack?.prefix}$alias';
String get textWarpedPlaceholder => ':$textPlaceholder:'; String get textWarpedPlaceholder => ':$textPlaceholder:';
String get imageUrl => ServiceFinder.buildUrl( String get imageUrl => ServiceFinder.buildUrl(

View File

@@ -8,6 +8,9 @@ class StickerProvider extends GetxController {
final RxList<Sticker> availableStickers = RxList.empty(growable: true); final RxList<Sticker> availableStickers = RxList.empty(growable: true);
Future<void> refreshAvailableStickers() async { Future<void> refreshAvailableStickers() async {
availableStickers.clear();
aliasImageMapping.clear();
final client = ServiceFinder.configureClient('files'); final client = ServiceFinder.configureClient('files');
final resp = await client.get( final resp = await client.get(
'/stickers/manifest?take=100', '/stickers/manifest?take=100',
@@ -20,7 +23,7 @@ class StickerProvider extends GetxController {
for (final pack in out) { for (final pack in out) {
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) { for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
sticker.pack = pack; sticker.pack = pack;
aliasImageMapping['${pack.prefix}${sticker.alias}'.camelCase!] = aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
sticker.imageUrl; sticker.imageUrl;
availableStickers.add(sticker); availableStickers.add(sticker);
} }

View File

@@ -70,7 +70,7 @@ class _StickerScreenState extends State<StickerScreen> {
); );
return ListTile( return ListTile(
title: Text(item.name), title: Text(item.name),
subtitle: Text(':${'$prefix${item.alias}'.camelCase}:'), subtitle: Text(item.textWarpedPlaceholder),
contentPadding: const EdgeInsets.only(left: 16, right: 14), contentPadding: const EdgeInsets.only(left: 16, right: 14),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -174,9 +174,10 @@ class _StickerScreenState extends State<StickerScreen> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
children: item.stickers children: item.stickers?.map((x) {
?.map((x) => _buildEmoteEntry(x, item.prefix)) x.pack = item;
.toList() ?? return _buildEmoteEntry(x, item.prefix);
}).toList() ??
List.empty(), List.empty(),
); );
}, },

View File

@@ -20,7 +20,6 @@ import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/channel/channel_call_indicator.dart'; import 'package:solian/widgets/channel/channel_call_indicator.dart';
import 'package:solian/widgets/chat/call/chat_call_action.dart'; import 'package:solian/widgets/chat/call/chat_call_action.dart';
import 'package:solian/widgets/chat/chat_event.dart';
import 'package:solian/widgets/chat/chat_event_list.dart'; import 'package:solian/widgets/chat/chat_event_list.dart';
import 'package:solian/widgets/chat/chat_message_input.dart'; import 'package:solian/widgets/chat/chat_message_input.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
@@ -39,7 +38,10 @@ class ChannelChatScreen extends StatefulWidget {
State<ChannelChatScreen> createState() => _ChannelChatScreenState(); State<ChannelChatScreen> createState() => _ChannelChatScreenState();
} }
class _ChannelChatScreenState extends State<ChannelChatScreen> { class _ChannelChatScreenState extends State<ChannelChatScreen>
with WidgetsBindingObserver {
DateTime? _isOutOfSyncSince;
bool _isBusy = false; bool _isBusy = false;
int? _accountId; int? _accountId;
@@ -123,20 +125,38 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}); });
} }
void _keepUpdateWithServer() {
_getOngoingCall();
_chatController.getEvents(_channel!, widget.realm);
setState(() => _isOutOfSyncSince = null);
}
Event? _messageToReplying; Event? _messageToReplying;
Event? _messageToEditing; Event? _messageToEditing;
Widget buildHistoryBody(Event item, {bool isMerged = false}) { @override
return ChatEvent( void didChangeAppLifecycleState(AppLifecycleState state) {
key: Key('m${item.uuid}'), switch (state) {
item: item, case AppLifecycleState.resumed:
isMerged: isMerged, if (_isOutOfSyncSince == null) break;
chatController: _chatController, if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 60) break;
); _keepUpdateWithServer();
break;
case AppLifecycleState.paused:
if (mounted) {
setState(() => _isOutOfSyncSince = DateTime.now());
}
break;
default:
break;
}
} }
@override @override
void initState() { void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_accountId = Get.find<AuthProvider>().userProfile.value!['id']; _accountId = Get.find<AuthProvider>().userProfile.value!['id'];
_chatController = ChatEventController(); _chatController = ChatEventController();
@@ -147,21 +167,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_chatController.getEvents(_channel!, widget.realm); _chatController.getEvents(_channel!, widget.realm);
_listenMessages(); _listenMessages();
}); });
super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _channel == null) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: const Center(
child: CircularProgressIndicator(),
),
);
}
String title = _channel?.name ?? 'loading'.tr; String title = _channel?.name ?? 'loading'.tr;
String? placeholder; String? placeholder;
@@ -185,7 +194,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
actions: [ actions: [
const BackgroundStateWidget(), const BackgroundStateWidget(),
Builder(builder: (context) { Builder(builder: (context) {
if (_isBusy) return const SizedBox(); if (_isBusy || _channel == null) return const SizedBox();
return ChatCallButton( return ChatCallButton(
realm: _channel!.realm, realm: _channel!.realm,
channel: _channel!, channel: _channel!,
@@ -195,6 +205,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
IconButton( IconButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onPressed: () { onPressed: () {
if (_channel == null) return;
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
'channelDetail', 'channelDetail',
@@ -219,66 +231,87 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
], ],
), ),
body: Column( body: Builder(builder: (context) {
children: [ if (_isBusy || _channel == null) {
if (_ongoingCall != null) return const Center(
ChannelCallIndicator( child: CircularProgressIndicator(),
channel: _channel!, );
ongoingCall: _ongoingCall!, }
return Column(
children: [
if (_ongoingCall != null)
ChannelCallIndicator(
channel: _channel!,
ongoingCall: _ongoingCall!,
),
Expanded(
child: ChatEventList(
scope: widget.realm,
channel: _channel!,
chatController: _chatController,
onEdit: (item) {
setState(() => _messageToEditing = item);
},
onReply: (item) {
setState(() => _messageToReplying = item);
},
),
), ),
Expanded( if (_isOutOfSyncSince != null)
child: ChatEventList( ListTile(
scope: widget.realm, tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
channel: _channel!, leading: const Icon(Icons.history_toggle_off),
chatController: _chatController, title: Text('messageOutOfSync'.tr),
onEdit: (item) { subtitle: Text('messageOutOfSyncCaption'.tr),
setState(() => _messageToEditing = item); onTap: _isBusy
}, ? null
onReply: (item) { : () {
setState(() => _messageToReplying = item); _keepUpdateWithServer();
}, },
), ),
), Obx(() {
Obx(() { if (_chatController.isLoading.isTrue) {
if (_chatController.isLoading.isTrue) { return const LinearProgressIndicator().animate().slideY();
return const LinearProgressIndicator().animate().slideY(); } else {
} else { return const SizedBox();
return const SizedBox(); }
} }),
}), ClipRect(
ClipRect( child: BackdropFilter(
child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), child: SafeArea(
child: SafeArea( child: ChatMessageInput(
child: ChatMessageInput( edit: _messageToEditing,
edit: _messageToEditing, reply: _messageToReplying,
reply: _messageToReplying, realm: widget.realm,
realm: widget.realm, placeholder: placeholder,
placeholder: placeholder, channel: _channel!,
channel: _channel!, onSent: (Event item) {
onSent: (Event item) { setState(() {
setState(() { _chatController.addPendingEvent(item);
_chatController.addPendingEvent(item); });
}); },
}, onReset: () {
onReset: () { setState(() {
setState(() { _messageToReplying = null;
_messageToReplying = null; _messageToEditing = null;
_messageToEditing = null; });
}); },
}, ),
), ),
), ),
), ),
), ],
], );
), }),
); );
} }
@override @override
void dispose() { void dispose() {
_subscription?.cancel(); _subscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
} }

View File

@@ -370,4 +370,7 @@ const i18nEnglish = {
'callStatusDisconnected': 'Disconnected', 'callStatusDisconnected': 'Disconnected',
'callStatusConnecting': 'Connecting', 'callStatusConnecting': 'Connecting',
'callStatusReconnected': 'Reconnecting', 'callStatusReconnected': 'Reconnecting',
'messageOutOfSync': 'May Out of Sync with Server',
'messageOutOfSyncCaption':
'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.',
}; };

View File

@@ -337,4 +337,6 @@ const i18nSimplifiedChinese = {
'callStatusDisconnected': '已断开', 'callStatusDisconnected': '已断开',
'callStatusConnecting': '连接中', 'callStatusConnecting': '连接中',
'callStatusReconnected': '重连中', 'callStatusReconnected': '重连中',
'messageOutOfSync': '消息可能与服务器脱节',
'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。',
}; };

View File

@@ -70,7 +70,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
'/attachments/${widget.item.id}', '/attachments/${widget.item.id}',
); );
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb || PlatformInfo.isDesktop) {
await launchUrlString(url); await launchUrlString(url);
return; return;
} }

View File

@@ -244,6 +244,7 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_showContent = widget.autoload;
} }
@override @override

View File

@@ -3,7 +3,7 @@ import 'dart:ui';
import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/carousel_slider.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart' hide CarouselController;
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';

View File

@@ -159,8 +159,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
var track = await LocalVideoTrack.createScreenShareTrack( var track = await LocalVideoTrack.createScreenShareTrack(
ScreenShareCaptureOptions( ScreenShareCaptureOptions(
captureScreenAudio: true,
sourceId: source.id, sourceId: source.id,
maxFrameRate: 15.0, maxFrameRate: 30.0,
), ),
); );
await _participant.publishVideoTrack(track); await _participant.publishVideoTrack(track);
@@ -174,6 +175,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
var track = await LocalVideoTrack.createScreenShareTrack( var track = await LocalVideoTrack.createScreenShareTrack(
const ScreenShareCaptureOptions( const ScreenShareCaptureOptions(
useiOSBroadcastExtension: true, useiOSBroadcastExtension: true,
captureScreenAudio: true,
maxFrameRate: 30.0, maxFrameRate: 30.0,
), ),
); );

View File

@@ -23,16 +23,19 @@ class ChatEventMessage extends StatelessWidget {
final body = EventMessageBody.fromJson(item.body); final body = EventMessageBody.fromJson(item.body);
final hasAttachment = body.attachments?.isNotEmpty ?? false; final hasAttachment = body.attachments?.isNotEmpty ?? false;
if (body.text.isEmpty && hasAttachment) { if (body.text.isEmpty &&
hasAttachment &&
!isContentPreviewing &&
!isQuote) {
final unFocusColor = final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return Row( return Row(
children: [ children: [
Icon( Icon(
Icons.attachment, Icons.file_copy,
size: 18, size: 15,
color: unFocusColor, color: unFocusColor,
).paddingOnly(right: 6), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams( 'attachmentHint'.trParams(
{'count': body.attachments?.length.toString() ?? 0.toString()}, {'count': body.attachments?.length.toString() ?? 0.toString()},

View File

@@ -129,12 +129,14 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
client = auth.configureClient('messaging'); client = auth.configureClient('messaging');
if (_textController.text.trim().isEmpty && _attachments.isEmpty) return;
const uuid = Uuid(); const uuid = Uuid();
final payload = { final payload = {
'uuid': uuid.v4(), 'uuid': uuid.v4(),
'type': _editTo == null ? 'messages.new' : 'messages.edit', 'type': _editTo == null ? 'messages.new' : 'messages.edit',
'body': { 'body': {
'text': _textController.text, 'text': _textController.text.trim(),
'algorithm': 'plain', 'algorithm': 'plain',
'attachments': List.from(_attachments), 'attachments': List.from(_attachments),
'related_users': [ 'related_users': [
@@ -239,7 +241,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
if (suggestion.type == 'emotes') { if (suggestion.type == 'emotes') {
insertText = suggestion.content; insertText = suggestion.content;
startText = replaceText.replaceFirstMapped( startText = replaceText.replaceFirstMapped(
RegExp(r':(?:([a-z0-9_+-]+)~)?([a-z0-9_+-]+)$'), RegExp(r':(?:([-\w]+)~)?([-\w]+)$'),
(Match m) => insertText, (Match m) => insertText,
); );
} }
@@ -247,7 +249,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
if (suggestion.type == 'users') { if (suggestion.type == 'users') {
insertText = suggestion.content; insertText = suggestion.content;
startText = replaceText.replaceFirstMapped( startText = replaceText.replaceFirstMapped(
RegExp(r'(?:\s|^)@([a-z0-9_+-]+)$'), RegExp(r'(?:\s|^)@([-\w]+)$'),
(Match m) => insertText, (Match m) => insertText,
); );
} }
@@ -349,15 +351,17 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
final searchText = _textController.text final searchText = _textController.text
.substring(0, _textController.selection.baseOffset); .substring(0, _textController.selection.baseOffset);
final emojiMatch = final emojiMatch = RegExp(r':(?:([-\w]+)~)?([-\w]+)$')
RegExp(r':(?:([a-z0-9_+-]+)~)?([a-z0-9_+-]+)$') .firstMatch(searchText);
.firstMatch(searchText);
if (emojiMatch != null) { if (emojiMatch != null) {
final StickerProvider stickers = Get.find(); final StickerProvider stickers = Get.find();
final emoteSearch = emojiMatch[2]!; final emoteSearch = emojiMatch[2]!;
return stickers.availableStickers return stickers.availableStickers
.where((x) => .where(
x.textWarpedPlaceholder.contains(emoteSearch)) (x) => x.textWarpedPlaceholder
.toUpperCase()
.contains(emoteSearch.toUpperCase()),
)
.map( .map(
(x) => ChatMessageSuggestion( (x) => ChatMessageSuggestion(
type: 'emotes', type: 'emotes',
@@ -379,8 +383,8 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
.toList(); .toList();
} }
final userMatch = RegExp(r'(?:\s|^)@([a-z0-9_+-]+)$') final userMatch =
.firstMatch(searchText); RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText);
if (userMatch != null) { if (userMatch != null) {
final userSearch = userMatch[1]!.toLowerCase(); final userSearch = userMatch[1]!.toLowerCase();
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();

View File

@@ -24,9 +24,9 @@ class MarkdownTextContent extends StatelessWidget {
}); });
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final emojiMatch = RegExp(r':([a-z0-9_+-]+):').allMatches(content); final emojiRegex = RegExp(r':([-\w]+):');
final isOnlyEmoji = final emojiMatch = emojiRegex.allMatches(content);
content.replaceAll(RegExp(r':([a-z0-9_+-]+):'), '').trim().isEmpty; final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
return Markdown( return Markdown(
shrinkWrap: true, shrinkWrap: true,
@@ -88,20 +88,40 @@ class MarkdownTextContent extends StatelessWidget {
final segments = url.replaceFirst('solink://', '').split('/'); final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) { switch (segments[0]) {
case 'stickers': case 'stickers':
double radius = 8;
final StickerProvider sticker = Get.find(); final StickerProvider sticker = Get.find();
url = sticker.aliasImageMapping[segments[1]]!; url = sticker.aliasImageMapping[segments[1].toUpperCase()]!;
if (emojiMatch.length <= 1 && isOnlyEmoji) { if (emojiMatch.length <= 1 && isOnlyEmoji) {
width = 112; width = 128;
height = 112; height = 128;
} else if (emojiMatch.length <= 3 && isOnlyEmoji) { } else if (emojiMatch.length <= 3 && isOnlyEmoji) {
width = 56; width = 32;
height = 56; height = 32;
} else { } else {
width = 28; radius = 4;
height = 28; width = 16;
height = 16;
} }
fit = BoxFit.contain; fit = BoxFit.contain;
break; return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
fit: fit,
)
: Image.network(
url,
width: width,
height: height,
fit: fit,
),
),
).paddingSymmetric(vertical: 4);
case 'attachments': case 'attachments':
const radius = BorderRadius.all(Radius.circular(8)); const radius = BorderRadius.all(Radius.circular(8));
return LimitedBox( return LimitedBox(
@@ -161,12 +181,12 @@ class _UserNameCardInlineSyntax extends InlineSyntax {
} }
class _CustomEmoteInlineSyntax extends InlineSyntax { class _CustomEmoteInlineSyntax extends InlineSyntax {
_CustomEmoteInlineSyntax() : super(r':([a-z0-9_+-]+):'); _CustomEmoteInlineSyntax() : super(r':([-\w]+):');
@override @override
bool onMatch(markdown.InlineParser parser, Match match) { bool onMatch(markdown.InlineParser parser, Match match) {
final StickerProvider sticker = Get.find(); final StickerProvider sticker = Get.find();
final alias = match[1]!; final alias = match[1]!.toUpperCase();
if (sticker.aliasImageMapping[alias] == null) { if (sticker.aliasImageMapping[alias] == null) {
parser.advanceBy(1); parser.advanceBy(1);
return false; return false;

View File

@@ -4,17 +4,17 @@ import 'package:get/utils.dart';
abstract class AppNavigation { abstract class AppNavigation {
static List<AppNavigationDestination> destinations = [ static List<AppNavigationDestination> destinations = [
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.home), icon: Icons.home,
label: 'home'.tr, label: 'home'.tr,
page: 'home', page: 'home',
), ),
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.workspaces), icon: Icons.workspaces,
label: 'realms'.tr, label: 'realms'.tr,
page: 'realms', page: 'realms',
), ),
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.forum), icon: Icons.forum,
label: 'channelTypeDirect'.tr, label: 'channelTypeDirect'.tr,
page: 'chat', page: 'chat',
), ),
@@ -25,7 +25,7 @@ abstract class AppNavigation {
} }
class AppNavigationDestination { class AppNavigationDestination {
final Widget icon; final IconData icon;
final String label; final String label;
final String page; final String page;

View File

@@ -3,16 +3,15 @@ import 'package:get/get.dart';
import 'package:solian/models/account_status.dart'; import 'package:solian/models/account_status.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_status_action.dart'; import 'package:solian/widgets/account/account_status_action.dart';
import 'package:solian/widgets/channel/channel_list.dart';
import 'package:solian/widgets/navigation/app_navigation.dart'; import 'package:solian/widgets/navigation/app_navigation.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:solian/widgets/navigation/app_navigation_regions.dart';
class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawer extends StatefulWidget {
final String? routeName; final String? routeName;
@@ -24,12 +23,8 @@ class AppNavigationDrawer extends StatefulWidget {
} }
class _AppNavigationDrawerState extends State<AppNavigationDrawer> { class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
int? _selectedIndex = 0;
AccountStatus? _accountStatus; AccountStatus? _accountStatus;
late final ChannelProvider _channels;
Future<void> _getStatus() async { Future<void> _getStatus() async {
final StatusProvider provider = Get.find(); final StatusProvider provider = Get.find();
@@ -41,15 +36,6 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}); });
} }
void _detectSelectedIndex() {
if (widget.routeName == null) return;
final nameList = AppNavigation.destinations.map((x) => x.page).toList();
final idx = nameList.indexOf(widget.routeName!);
_selectedIndex = idx != -1 ? idx : null;
}
void _closeDrawer() { void _closeDrawer() {
rootScaffoldKey.currentState!.closeDrawer(); rootScaffoldKey.currentState!.closeDrawer();
} }
@@ -59,7 +45,6 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
onPressed: () { onPressed: () {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('settings');
setState(() => _selectedIndex = null);
_closeDrawer(); _closeDrawer();
}); });
} }
@@ -67,175 +52,139 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_channels = Get.find();
_detectSelectedIndex();
_getStatus(); _getStatus();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_detectSelectedIndex();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
return NavigationDrawer( return Drawer(
backgroundColor: backgroundColor:
SolianTheme.isLargeScreen(context) ? Colors.transparent : null, SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
selectedIndex: _selectedIndex, child: SafeArea(
onDestinationSelected: (idx) { child: Column(
setState(() => _selectedIndex = idx); children: [
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page); Obx(() {
_closeDrawer(); if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
}, return ListTile(
children: [ contentPadding: const EdgeInsets.symmetric(horizontal: 28),
Obx(() { leading: const Icon(Icons.account_circle),
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) { title: Text('guest'.tr),
return ListTile( subtitle: Text('unsignedIn'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 28), trailing: _buildSettingButton(),
leading: const Icon(Icons.account_circle), onTap: () {
title: Text('guest'.tr), AppRouter.instance.goNamed('account');
subtitle: Text('unsignedIn'.tr), _closeDrawer();
trailing: _buildSettingButton(), },
onTap: () {
AppRouter.instance.goNamed('account');
setState(() => _selectedIndex = null);
_closeDrawer();
},
);
}
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(
auth.userProfile.value!['nick'],
maxLines: 1,
overflow: TextOverflow.fade,
),
subtitle: Builder(
builder: (context) {
if (_accountStatus == null) {
return Text('loading'.tr);
}
final info = StatusProvider.determineStatus(
_accountStatus!,
); );
return Text( }
info.$3,
return ListTile(
contentPadding: const EdgeInsets.only(left: 20, right: 20),
title: Text(
auth.userProfile.value!['nick'],
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
); ),
}, subtitle: Builder(
), builder: (context) {
leading: Obx(() { if (_accountStatus == null) {
final statusBadgeColor = _accountStatus != null return Text('loading'.tr);
? StatusProvider.determineStatus( }
final info = StatusProvider.determineStatus(
_accountStatus!, _accountStatus!,
).$2 );
: Colors.grey; return Text(
info.$3,
final RelationshipProvider relations = Get.find(); maxLines: 1,
final accountNotifications = relations.friendRequestCount.value; overflow: TextOverflow.fade,
);
return badges.Badge( },
badgeContent: Text(
accountNotifications.toString(),
style: const TextStyle(color: Colors.white),
), ),
showBadge: accountNotifications > 0, leading: Obx(() {
position: badges.BadgePosition.topEnd( final statusBadgeColor = _accountStatus != null
top: -10, ? StatusProvider.determineStatus(
end: -6, _accountStatus!,
), ).$2
child: badges.Badge( : Colors.grey;
showBadge: _accountStatus != null,
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
position: badges.BadgePosition.bottomEnd(
bottom: 0,
end: -2,
),
child: AccountAvatar(
content: auth.userProfile.value!['avatar'],
),
),
);
}),
trailing: _buildSettingButton(),
onTap: () {
AppRouter.instance.goNamed('account');
setState(() => _selectedIndex = null);
_closeDrawer();
},
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => AccountStatusAction(
currentStatus: _accountStatus!.status,
),
).then((val) {
if (val == true) _getStatus();
});
},
);
}).paddingOnly(top: 8),
const Divider(thickness: 0.3, height: 1).paddingOnly(
bottom: 12,
top: 8,
),
...AppNavigation.destinations.map(
(e) => NavigationDrawerDestination(
icon: e.icon,
label: Text(e.label),
),
),
const Divider(thickness: 0.3, height: 1).paddingOnly(
top: 12,
),
Obx(() {
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
return const SizedBox();
}
final selfId = auth.userProfile.value!['id']; final RelationshipProvider relations = Get.find();
final accountNotifications =
relations.friendRequestCount.value;
return Column( return badges.Badge(
children: [ badgeContent: Text(
Theme( accountNotifications.toString(),
data: Theme.of(context) style: const TextStyle(color: Colors.white),
.copyWith(dividerColor: Colors.transparent), ),
child: ExpansionTile( showBadge: accountNotifications > 0,
title: Text('channels'.tr), position: badges.BadgePosition.topEnd(
tilePadding: const EdgeInsets.symmetric(horizontal: 24), top: -10,
children: [ end: -6,
Obx( ),
() => SizedBox( child: badges.Badge(
height: 360, showBadge: _accountStatus != null,
child: RefreshIndicator( badgeStyle:
onRefresh: () => _channels.refreshAvailableChannel(), badges.BadgeStyle(badgeColor: statusBadgeColor),
child: ChannelListWidget( position: badges.BadgePosition.bottomEnd(
channels: _channels.groupChannels, bottom: 0,
selfId: selfId, end: -2,
isDense: true, ),
useReplace: true, child: AccountAvatar(
onSelected: (_) { content: auth.userProfile.value!['avatar'],
setState(() => _selectedIndex = null);
_closeDrawer();
},
),
),
), ),
), ),
], );
), }),
trailing: _buildSettingButton(),
onTap: () {
AppRouter.instance.goNamed('account');
_closeDrawer();
},
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => AccountStatusAction(
currentStatus: _accountStatus!.status,
),
).then((val) {
if (val == true) _getStatus();
});
},
);
}).paddingSymmetric(vertical: 8),
const Divider(thickness: 0.3, height: 1),
Column(
children: AppNavigation.destinations
.map(
(e) => ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
leading: Icon(e.icon, size: 20).paddingAll(2),
title: Text(e.label),
enabled: true,
onTap: () {
AppRouter.instance.goNamed(e.page);
_closeDrawer();
},
),
)
.toList(),
).paddingSymmetric(vertical: 8),
const Divider(thickness: 0.3, height: 1),
Expanded(
child: AppNavigationRegions(
onSelected: (item) {
_closeDrawer();
},
), ),
], ),
); ],
}), ),
], ),
); );
} }
} }

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart';
import 'package:collection/collection.dart';
class AppNavigationRegions extends StatelessWidget {
final Function(Channel item) onSelected;
const AppNavigationRegions({super.key, required this.onSelected});
void _gotoChannel(Channel item) {
AppRouter.instance.pushReplacementNamed(
'channelChat',
pathParameters: {'alias': item.alias},
queryParameters: {
if (item.realmId != null) 'realm': item.realm!.alias,
},
);
onSelected(item);
}
Widget _buildEntry(BuildContext context, Channel item) {
const padding = EdgeInsets.symmetric(horizontal: 20);
return ListTile(
minTileHeight: 0,
leading: const Icon(Icons.tag_outlined),
contentPadding: padding,
title: Text(item.name),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => _gotoChannel(item),
);
}
@override
Widget build(BuildContext context) {
final ChannelProvider channels = Get.find();
return Obx(() {
final List<Channel> noRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId == null)
.toList();
final List<Channel> hasRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId != null)
.toList();
return CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 8)),
SliverList.builder(
itemCount: noRealmGroupChannels.length,
itemBuilder: (context, index) {
final element = noRealmGroupChannels[index];
return _buildEntry(context, element);
},
),
SliverList.list(
children: hasRealmGroupChannels
.groupListsBy((x) => x.realm)
.entries
.map((element) {
return ExpansionTile(
minTileHeight: 0,
tilePadding: const EdgeInsets.only(left: 20, right: 24),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
title: Text(element.value.first.realm!.name),
leading: const Icon(Icons.workspaces, size: 16)
.paddingSymmetric(horizontal: 4),
children:
element.value.map((x) => _buildEntry(context, x)).toList(),
);
}).toList(),
),
],
);
});
}
}

View File

@@ -436,6 +436,7 @@ class _PostItemState extends State<PostItem> {
left: 16, left: 16,
), ),
AttachmentList( AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(), parentId: widget.item.id.toString(),
attachmentsId: attachments, attachmentsId: attachments,
isGrid: attachments.length > 1, isGrid: attachments.length > 1,

View File

@@ -226,7 +226,7 @@ packages:
source: hosted source: hosted
version: "4.10.0" version: "4.10.0"
collection: collection:
dependency: transitive dependency: "direct main"
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
@@ -405,10 +405,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.6" version: "8.0.7"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@@ -740,10 +740,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: f46bd76cef6e8d787dc707d0c591f0e89c912a2970c7b5e68a55b9cca1bdde4c sha256: "67faa07cf49392b50b1aa14590a83caa64d2109345fabd29899dcd8da8538348"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.6" version: "0.11.6+hotfix.1"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -788,10 +788,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722 sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.2" version: "14.2.3"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -876,10 +876,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: "3fe99e3a459b969f657565a853353bdea7e3d3cae34f7dd124836267d426ec87" sha256: c0e72ecd170b00a5590bb71238d57dc8ad22ee14c60c6b0d1a4e05cafbc5db4b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.12+10" version: "0.8.12+11"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@@ -1276,10 +1276,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54 sha256: eaf2a1ec4472775451e88ca6a7b86559ef2f1d1ed903942ed135e38ea0097dca
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.0.7" version: "12.0.8"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
@@ -1300,10 +1300,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.1" version: "4.2.2"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1897,10 +1897,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049 sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.3"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:

View File

@@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.1+8 version: 1.2.1+9
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@@ -68,6 +68,7 @@ dependencies:
async: ^2.11.0 async: ^2.11.0
field_suggestion: ^0.2.5 field_suggestion: ^0.2.5
flutter_typeahead: ^5.2.0 flutter_typeahead: ^5.2.0
collection: ^1.18.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: