Compare commits

...

11 Commits

Author SHA1 Message Date
cb05ff2e9e 🚀 Launch 2.2.1+44 2025-01-01 19:38:57 +08:00
f614da7918 💄 Optimize attachment list 2025-01-01 17:57:41 +08:00
a3c8dafff9 User typing status
🐛 Bug fixes
2025-01-01 16:45:37 +08:00
fa978a7cd1 🐛 Fix notification mark all as read issue 2025-01-01 11:50:06 +08:00
aaa0a562b4 💄 Fix transparent icon color issue 2025-01-01 01:48:35 +08:00
590a4ce2a6 🐛 Bug fixes on chat message rendering 2025-01-01 01:11:35 +08:00
f26edce071 🚀 Launch 2.2.1+43 2024-12-29 23:58:31 +08:00
603799ea32 🐛 Fix high quality icon issue 2024-12-29 23:52:48 +08:00
a32baf7798 Able to set attachment alt text 2024-12-29 23:50:56 +08:00
498c9af663 💄 Optimize chatting input
 Rollback universal image
2024-12-29 23:30:29 +08:00
202dbff6d3 🐛 Bug fixes 2024-12-29 23:11:50 +08:00
27 changed files with 727 additions and 422 deletions

View File

@ -281,7 +281,12 @@
"one": "{} attachment",
"other": "{} attachments"
},
"messageTyping": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"fieldAttachmentRandomId": "Random ID",
"fieldAttachmentAlt": "Alternative text",
"addAttachmentFromAlbum": "Add from album",
"addAttachmentFromClipboard": "Paste file",
"addAttachmentFromCameraPhoto": "Take photo",
@ -293,6 +298,7 @@
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
"attachmentCompressVideo": "Re-encode video",
"attachmentSetThumbnail": "Set thumbnail",
"attachmentSetAlt": "Set alternative text",
"attachmentCopyRandomId": "Copy RID",
"attachmentUpload": "Upload",
"attachmentInputDialog": "Upload attachments",

View File

@ -279,7 +279,12 @@
"one": "{} 个附件",
"other": "{} 个附件"
},
"messageTyping": {
"one": "{} 正在输入",
"other": "{} 正在输入"
},
"fieldAttachmentRandomId": "访问 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "从相册中添加附件",
"addAttachmentFromClipboard": "粘贴附件",
"addAttachmentFromCameraPhoto": "拍摄照片",
@ -291,6 +296,7 @@
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
"attachmentCompressVideo": "重新编码视频",
"attachmentSetThumbnail": "设置缩略图",
"attachmentSetAlt": "设置概述文字",
"attachmentCopyRandomId": "复制访问 ID",
"attachmentUpload": "上传",
"attachmentInputDialog": "上传附件",

View File

@ -280,6 +280,7 @@
"other": "{} 個附件"
},
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +292,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件",

View File

@ -280,6 +280,7 @@
"other": "{} 個附件"
},
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +292,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件",

View File

@ -211,6 +211,9 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
@ -256,6 +259,7 @@ DEPENDENCIES:
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@ -343,6 +347,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
@ -401,6 +407,7 @@ SPEC CHECKSUMS:
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:collection/collection.dart';
@ -11,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/types/websocket.dart';
import 'package:uuid/uuid.dart';
class ChatMessageController extends ChangeNotifier {
@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
int? messageTotal;
bool get isAllLoaded =>
messageTotal != null && messages.length >= messageTotal!;
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
String? _boxKey;
SnChannel? channel;
@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
/// Stored as a list of nonce to provide the loading state
final List<String> unconfirmedMessages = List.empty(growable: true);
Box<SnChatMessage>? get _box =>
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
final List<SnChannelMember> typingMembers = List.empty(growable: true);
final Map<int, Timer> typingInactiveTimer = {};
Future<void> initialize(SnChannel chan) async {
channel = chan;
@ -78,22 +81,17 @@ class ChatMessageController extends ChangeNotifier {
if (event.payload?['channel_id'] != channel?.id) break;
final member = SnChannelMember.fromJson(event.payload!['member']);
if (member.id == profile?.id) break;
// TODO impl typing users
// if (!_typingUsers.any((x) => x.id == member.id)) {
// setState(() {
// _typingUsers.add(member);
// });
// }
// _typingInactiveTimer[member.id]?.cancel();
// _typingInactiveTimer[member.id] = Timer(
// const Duration(seconds: 3),
// () {
// setState(() {
// _typingUsers.removeWhere((x) => x.id == member.id);
// _typingInactiveTimer.remove(member.id);
// });
// },
// );
if (!typingMembers.any((x) => x.id == member.id)) {
typingMembers.add(member);
print('Typing member: ${typingMembers.map((ele) => member.id)}');
notifyListeners();
}
typingInactiveTimer[member.id]?.cancel();
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
typingMembers.removeWhere((x) => x.id == member.id);
typingInactiveTimer.remove(member.id);
notifyListeners();
});
}
});
@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
notifyListeners();
}
Timer? _typingNotifyTimer;
bool _typingStatus = false;
Future<void> _sendTypingStatusPackage() async {
_ws.conn?.sink.add(jsonEncode(
WebSocketPackage(
method: 'status.typing',
endpoint: 'im',
payload: {
'channel_id': channel!.id,
},
).toJson(),
));
}
void pingTypingStatus() {
if (!_typingStatus) {
_sendTypingStatusPackage();
_typingStatus = true;
}
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
_typingNotifyTimer?.cancel();
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
_typingStatus = false;
});
}
}
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
if (_box == null) return;
await _box!.putAll({
@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
switch (message.type) {
case 'messages.edit':
if (message.relatedEventId != null) {
final idx =
messages.indexWhere((x) => x.id == message.relatedEventId);
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
if (idx != -1) {
final newBody = message.body;
newBody.remove('related_event');
@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
'algorithm': 'plain',
if (quoteId != null) 'quote_event': quoteId,
if (relatedId != null) 'related_event': relatedId,
if (attachments != null && attachments.isNotEmpty)
'attachments': attachments,
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
};
// Mock the message locally
@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
if (out == null) {
try {
final resp = await _sn.client
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
out = SnChatMessage.fromJson(resp.data);
_saveMessageToLocal([out]);
} catch (_) {
@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
bool forceRemote = false,
}) async {
late List<SnChatMessage> out;
if (_box != null &&
(_box!.length >= take + offset || forceLocal) &&
!forceRemote) {
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
out = _box!.keys
.toList()
.cast<int>()
@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
quoteEvent: quoteEvent,
attachments: attachments
.where(
(ele) =>
out[i].body['attachments']?.contains(ele?.rid) ?? false,
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
)
.toList(),
),
@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
}
// Preload sender accounts
final accountId = out
.where((ele) => ele.sender.accountId >= 0)
.map((ele) => ele.sender.accountId)
.toSet();
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
await _ud.listAccount(accountId);
return out;

View File

@ -104,7 +104,7 @@ class PostWriteMedia {
if (attachment != null) {
final sn = context.read<SnNetworkProvider>();
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
if (width != null && height != null) {
if (width != null && height != null && !kIsWeb) {
return ResizeImage(
provider,
width: width,
@ -213,11 +213,11 @@ class PostWriteController extends ChangeNotifier {
aliasController.text = post.alias ?? '';
publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? []);
invisibleUsers = List.from(post.invisibleUsersList ?? []);
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
visibility = post.visibility;
tags = List.from(post.tags.map((ele) => ele.alias));
categories = List.from(post.categories.map((ele) => ele.alias));
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
@ -344,9 +344,10 @@ class PostWriteController extends ChangeNotifier {
if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(),
'tags': tags.map((ele) => {'alias': ele}).toList(),
'categories': categories.map((ele) => {'alias': ele}).toList(),
'attachments':
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
'visibility': visibility,
'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers,

View File

@ -0,0 +1,3 @@
class StickerProvider {
}

View File

@ -87,7 +87,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
try {
final resp = await sn.client.request(
widget.editingChannelAlias != null
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
? '/cgi/im/channels/$scope/${_editingChannel!.id}'
: '/cgi/im/channels/$scope',
data: payload,
options: Options(

View File

@ -17,6 +17,7 @@ import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_prejoin.dart';
import 'package:surface/widgets/chat/chat_message.dart';
import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -280,11 +281,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
Expanded(
child: InfiniteList(
reverse: true,
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 12,
),
padding: const EdgeInsets.only(top: 12),
hasReachedMax: _messageController.isAllLoaded,
itemCount: _messageController.messages.length,
isLoading: _messageController.isLoading,
@ -310,23 +307,20 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
return Align(
alignment: Alignment.centerLeft,
child: Container(
constraints: BoxConstraints(maxWidth: 480),
child: ChatMessage(
data: message,
isMerged: canMerge,
hasMerged: canMergePrevious,
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
onReply: (value) {
_inputGlobalKey.currentState?.setReply(value);
},
onEdit: (value) {
_inputGlobalKey.currentState?.setEdit(value);
},
onDelete: (value) {
_inputGlobalKey.currentState?.deleteMessage(value);
},
),
child: ChatMessage(
data: message,
isMerged: canMerge,
hasMerged: canMergePrevious,
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
onReply: (value) {
_inputGlobalKey.currentState?.setReply(value);
},
onEdit: (value) {
_inputGlobalKey.currentState?.setEdit(value);
},
onDelete: (value) {
_inputGlobalKey.currentState?.deleteMessage(value);
},
),
);
},
@ -335,11 +329,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
if (!_messageController.isPending)
Material(
elevation: 2,
child: ChatMessageInput(
key: _inputGlobalKey,
otherMember: _otherMember,
controller: _messageController,
).padding(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
children: [
ChatTypingIndicator(controller: _messageController),
ChatMessageInput(
key: _inputGlobalKey,
otherMember: _otherMember,
controller: _messageController,
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
],
);

View File

@ -153,9 +153,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
}
}
class _HomeDashSpecialDayWidget extends StatelessWidget {
class _HomeDashSpecialDayWidget extends StatefulWidget {
const _HomeDashSpecialDayWidget();
@override
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
}
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
@override
Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
@ -165,21 +170,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
if (days.isNotEmpty) {
return Column(
spacing: 8,
children: days.map((ele) {
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
subtitle: Text(
DateFormat('y/M/d').format(DateTime.now().copyWith(
month: kSpecialDays[ele]!.$1,
day: kSpecialDays[ele]!.$2,
)),
),
),
).padding(bottom: 8);
}).toList());
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
subtitle: Text(
DateFormat('y/M/d').format(DateTime.now().copyWith(
month: kSpecialDays[ele]?.$1,
day: kSpecialDays[ele]?.$2,
)),
),
),
).padding(bottom: 8);
}).toList());
}
final nextOne = dayz.getNextSpecialDay();
@ -204,6 +208,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
separatorType: SeparatorType.symbol,
decoration: BoxDecoration(),
padding: EdgeInsets.zero,
onDone: () {
setState(() {});
},
),
const Gap(12),
Expanded(

View File

@ -82,24 +82,15 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (!mounted) return;
setState(() => _isSubmitting = true);
List<int> markList = List.empty(growable: true);
for (final element in _notifications) {
if (element.id <= 0) continue;
if (element.readAt != null) continue;
markList.add(element.id);
}
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/id/notifications/read', data: {
'messages': markList,
});
final resp = await sn.client.put('/cgi/id/notifications/read/all');
_notifications.clear();
_fetchNotifications();
if (!mounted) return;
context.showSnackbar(
'notificationMarkAllReadPrompt'.plural(markList.length),
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
);
} catch (err) {
if (!mounted) return;

View File

@ -365,30 +365,31 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: _writeController.temporaryRestored
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.restore, size: 20),
const Gap(8),
Expanded(child: Text('postLocalDraftRestored').tr()),
InkWell(
child: Text('dialogDismiss').tr(),
onTap: () {
_writeController.reset();
},
? Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
],
)
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.restore, size: 20),
const Gap(8),
Expanded(child: Text('postLocalDraftRestored').tr()),
InkWell(
child: Text('dialogDismiss').tr(),
onTap: () {
_writeController.reset();
},
),
],
))
: const SizedBox.shrink(),
)
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)

View File

@ -88,9 +88,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
title: Text(_realm?.name ?? 'loading'.tr()),
bottom: TabBar(
tabs: [
Tab(icon: const Icon(Symbols.home)),
Tab(icon: const Icon(Symbols.group)),
Tab(icon: const Icon(Symbols.settings)),
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
],
),
),

View File

@ -315,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
}
return MaterialDesktopVideoControlsTheme(
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
normal: MaterialDesktopVideoControlsThemeData(
buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white,
@ -324,14 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
MaterialDesktopCustomButton(
iconSize: 24,
onPressed: _toggleOriginal,
icon: Builder(builder: (context) {
return _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24);
}),
icon: Icon(
_showOriginal ? Symbols.high_quality : Symbols.sd,
size: 24,
),
),
],
),
fullscreen: const MaterialDesktopVideoControlsThemeData(),
child: MaterialVideoControlsTheme(
key: Key('material-video-controls-theme-$_showOriginal'),
normal: MaterialVideoControlsThemeData(
buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white,

View File

@ -15,10 +15,10 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data;
final bool bordered;
final bool gridded;
final bool noGrow;
final BoxFit fit;
final double? maxHeight;
final double? minWidth;
final double? maxWidth;
final EdgeInsets? padding;
const AttachmentList({
@ -26,10 +26,10 @@ class AttachmentList extends StatefulWidget {
required this.data,
this.bordered = false,
this.gridded = false,
this.noGrow = false,
this.fit = BoxFit.cover,
this.maxHeight,
this.minWidth,
this.maxWidth,
this.padding,
});
@ -106,76 +106,38 @@ class _AttachmentListState extends State<AttachmentList> {
}
if (widget.gridded) {
return Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: StaggeredGrid.count(
crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
onTap: () {
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
),
)
.toList(),
),
return Container(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
);
}
return AspectRatio(
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(),
child: Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ScrollConfiguration(
behavior: _AttachmentListScrollBehavior(),
child: ListView.separated(
shrinkWrap: true,
itemCount: widget.data.length,
itemBuilder: (context, idx) {
return Container(
constraints: constraints,
child: AspectRatio(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector(
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: StaggeredGrid.count(
crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data:
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),
@ -183,44 +145,76 @@ class _AttachmentListState extends State<AttachmentList> {
rootNavigator: true,
);
},
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
)
.toList(),
),
),
);
}
return Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ScrollConfiguration(
behavior: _AttachmentListScrollBehavior(),
child: ListView.separated(
padding: widget.padding,
shrinkWrap: true,
itemCount: widget.data.length,
itemBuilder: (context, idx) {
return Container(
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
child: AspectRatio(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector(
onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
),
),
Positioned(
right: 8,
bottom: 8,
child: Chip(
label: Text('${idx + 1}/${widget.data.length}'),
),
),
Positioned(
right: 8,
bottom: 8,
child: Chip(
label: Text('${idx + 1}/${widget.data.length}'),
),
],
),
),
],
),
),
);
},
separatorBuilder: (context, index) => const Gap(8),
padding: widget.padding,
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
),
),
);
},
separatorBuilder: (context, index) => const Gap(8),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
),
),
);

View File

@ -231,7 +231,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
children: [
IgnorePointer(
child: AccountImage(
content: account!.avatar,
content: account?.avatar,
radius: 19,
),
),
@ -246,7 +246,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account.nick,
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],

View File

@ -0,0 +1,86 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/widgets/dialog.dart';
class PendingAttachmentAltDialog extends StatefulWidget {
final PostWriteMedia media;
const PendingAttachmentAltDialog({super.key, required this.media});
@override
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
}
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
final _contentController = TextEditingController();
@override
void initState() {
super.initState();
_contentController.text = widget.media.attachment!.alt;
}
bool _isBusy = false;
Future<void> _performAction() async {
if (_isBusy) return;
setState(() => _isBusy = true);
try {
final attach = context.read<SnAttachmentProvider>();
final result = await attach.updateOne(
widget.media.attachment!,
alt: _contentController.text,
);
if (!mounted) return;
attach.putCache([result]);
Navigator.pop(context, result);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
}
}
@override
void dispose() {
_contentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('attachmentSetAlt').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _contentController,
decoration: InputDecoration(
labelText: 'fieldAttachmentAlt'.tr(),
border: const UnderlineInputBorder(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () {
Navigator.pop(context);
},
child: Text('dialogDismiss'.tr()),
),
TextButton(
onPressed: _isBusy ? null : () => _performAction(),
child: Text('dialogConfirm'.tr()),
),
],
);
}
}

View File

@ -24,6 +24,7 @@ class ChatMessage extends StatelessWidget {
final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
final EdgeInsets padding;
const ChatMessage({
super.key,
@ -35,6 +36,7 @@ class ChatMessage extends StatelessWidget {
this.onReply,
this.onEdit,
this.onDelete,
this.padding = const EdgeInsets.only(left: 12, right: 12),
});
@override
@ -53,7 +55,7 @@ class ChatMessage extends StatelessWidget {
iconOnRightSwipe: Symbols.edit,
swipeSensitivity: 20,
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
child: ContextMenuArea(
contextMenu: ContextMenu(
entries: [
@ -87,84 +89,89 @@ class ChatMessage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged && !isCompact)
AccountImage(
content: user?.avatar,
)
else if (isMerged)
const Gap(40),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged)
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 6),
Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(6),
Text(
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
if (isCompact) const Gap(4),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
Padding(
padding: isCompact ? EdgeInsets.zero : padding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged && !isCompact)
AccountImage(
content: user?.avatar,
)
else if (isMerged)
const Gap(40),
const Gap(8),
Expanded(
child: Container(
constraints: BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 8),
Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(8),
Text(
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(data: data),
_ => _ChatMessageSystemNotify(data: data),
},
],
),
)
],
).opacity(isPending ? 0.5 : 1),
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
if (isCompact) const Gap(8),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(data: data),
_ => _ChatMessageSystemNotify(data: data),
},
],
),
),
)
],
).opacity(isPending ? 0.5 : 1),
),
if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
LinkPreviewWidget(text: data.body['text']!),
if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList(
data: data.preload!.attachments!,
bordered: true,
gridded: true,
noGrow: true,
maxHeight: 520,
padding: const EdgeInsets.only(top: 8),
maxHeight: 560,
maxWidth: 480,
minWidth: 480,
padding: padding.copyWith(top: 8),
),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
],
),
),

View File

@ -11,7 +11,6 @@ import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart';
class ChatMessageInput extends StatefulWidget {
@ -33,6 +32,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final TextEditingController _contentController = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_contentController.addListener(() {
if (_contentController.text.isNotEmpty) {
widget.controller.pingTypingStatus();
}
});
}
void setReply(SnChatMessage? value) {
setState(() => _replyingMessage = value);
}
@ -161,75 +170,82 @@ class ChatMessageInputState extends State<ChatMessageInput> {
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Padding(
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
child: _replyingMessage != null
? MaterialBanner(
padding: const EdgeInsets.only(left: 16.0),
leading: const Icon(Symbols.reply),
backgroundColor: Colors.transparent,
content: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_replyingMessage?.body['text'] != null)
MarkdownTextContent(
content: _replyingMessage?.body['text'],
),
],
child: _replyingMessage != null
? Container(
padding: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
actions: [
TextButton(
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.reply, size: 20),
const Gap(8),
Expanded(
child: Text(
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(16),
InkWell(
child: Text('cancel'.tr()),
onPressed: () {
onTap: () {
setState(() => _replyingMessage = null);
},
),
],
)
: const SizedBox.shrink(),
),
).padding(vertical: 8),
)
: const SizedBox.shrink(),
)
.height(_replyingMessage != null ? 54 + 8 : 0, animate: true)
.height(_replyingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Padding(
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
child: _editingMessage != null
? MaterialBanner(
padding: const EdgeInsets.only(left: 16.0),
leading: const Icon(Symbols.edit),
backgroundColor: Colors.transparent,
content: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_editingMessage?.body['text'] != null)
MarkdownTextContent(
content: _editingMessage?.body['text'],
),
],
child: _editingMessage != null
? Container(
padding: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
actions: [
TextButton(
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.edit, size: 20),
const Gap(8),
Expanded(
child: Text(
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(16),
InkWell(
child: Text('cancel'.tr()),
onPressed: () {
onTap: () {
_contentController.clear();
setState(() => _editingMessage = null);
},
),
],
)
: const SizedBox.shrink(),
),
).padding(vertical: 8),
)
: const SizedBox.shrink(),
)
.height(_editingMessage != null ? 54 + 8 : 0, animate: true)
.height(_editingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SizedBox(
height: 56,

View File

@ -0,0 +1,53 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/providers/user_directory.dart';
class ChatTypingIndicator extends StatelessWidget {
final ChatMessageController controller;
const ChatTypingIndicator({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
return StyledWidget(controller.typingMembers.isEmpty
? const SizedBox.shrink()
: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
children: [
const Icon(Symbols.more_horiz, weight: 600, size: 20),
const Gap(8),
Text(
'messageTyping'.plural(controller.typingMembers.length, args: [
controller.typingMembers
.map((ele) => (ele.nick?.isNotEmpty ?? false)
? ele.nick!
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
.join(', '),
]),
),
],
),
))
.height(controller.typingMembers.isNotEmpty ? 38 : 0, animate: true)
.animate(
const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn,
);
}
}

View File

@ -21,6 +21,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/dialog.dart';
@ -157,6 +158,16 @@ class PostMediaPendingList extends StatelessWidget {
onUpdate!(idx, result);
}
Future<void> _setAlt(BuildContext context, int idx) async {
final result = await showDialog<SnAttachment?>(
context: context,
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
);
if (result == null) return;
onUpdate!(idx, PostWriteMedia(result));
}
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
return ContextMenu(
@ -169,6 +180,14 @@ class PostMediaPendingList extends StatelessWidget {
_compressVideo(context, idx);
},
),
if (media.attachment != null)
MenuItem(
label: 'attachmentSetAlt'.tr(),
icon: Symbols.description,
onSelected: () {
_setAlt(context, idx);
},
),
if (media.attachment != null)
MenuItem(
label: 'attachmentBoost'.tr(),

View File

@ -1,4 +1,4 @@
import 'package:extended_image/extended_image.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -7,6 +7,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart';
// Keep this import to make the web image render work
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:surface/providers/config.dart';
class UniversalImage extends StatelessWidget {
@ -33,54 +34,64 @@ class UniversalImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final quality = filterQuality ?? context.read<ConfigProvider>().imageQuality;
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
return ExtendedImage.network(
url,
return Image(
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality,
image: kIsWeb
? UniversalImage.provider(url)
: ResizeImage(
UniversalImage.provider(url),
width: resizeWidth?.round(),
height: resizeHeight?.round(),
policy: ResizeImagePolicy.fit,
),
width: width,
height: height,
fit: fit,
cache: true,
compressionRatio: kIsWeb ? 1 : switch(quality) {
FilterQuality.high => 1,
FilterQuality.medium => 0.75,
FilterQuality.low => 0.5,
FilterQuality.none => 0.25,
},
filterQuality: quality,
enableLoadState: true,
retries: 3,
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.completed) {
return state.completedWidget;
} else if (state.extendedImageLoadState == LoadState.failed) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Container(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
state.lastException.toString(),
textAlign: TextAlign.center,
loadingBuilder: noProgressIndicator
? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: 0,
),
],
).center(),
),
);
}
return Center(
child: CircularProgressIndicator(
value: state.loadingProgress != null
? state.loadingProgress!.cumulativeBytesLoaded / state.loadingProgress!.expectedTotalBytes!
: null,
),
);
},
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
),
),
);
},
errorBuilder: noErrorWidget
? null
: (context, error, stackTrace) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Container(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
error.toString(),
textAlign: TextAlign.center,
),
],
).center(),
),
);
},
);
}
@ -88,10 +99,9 @@ class UniversalImage extends StatelessWidget {
// This place used to use network image or cached network image depending on the platform.
// But now the cached network image is working on every platform.
// So we just use it now.
return ExtendedNetworkImageProvider(
return CachedNetworkImageProvider(
url,
cache: true,
retries: 3,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
);
}
}

View File

@ -26,6 +26,7 @@ import path_provider_foundation
import screen_brightness_macos
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import video_compress
import wakelock_plus
@ -52,6 +53,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))

View File

@ -165,6 +165,9 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- video_compress (0.3.0):
@ -198,6 +201,7 @@ DEPENDENCIES:
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -267,6 +271,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_compress:
@ -311,6 +317,7 @@ SPEC CHECKSUMS:
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269

View File

@ -182,6 +182,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.9.3"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
cassowary:
dependency: transitive
description:
@ -430,22 +454,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
extended_image:
dependency: "direct main"
description:
name: extended_image
sha256: "93890a88d89ce017789f6c031c32ad8d2c685f1a5c25c169550746d973ca5e44"
url: "https://pub.dev"
source: hosted
version: "9.0.9"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7"
url: "https://pub.dev"
source: hosted
version: "4.0.5"
fading_edge_scrollview:
dependency: transitive
description:
@ -635,6 +643,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.2"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_colorpicker:
dependency: "direct main"
description:
@ -874,14 +890,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.2"
http_client_helper:
dependency: transitive
description:
name: http_client_helper
sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
http_multi_server:
dependency: transitive
description:
@ -1242,6 +1250,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
package_config:
dependency: transitive
description:
@ -1530,6 +1546,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
safe_local_storage:
dependency: transitive
description:
@ -1743,6 +1767,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
source: hosted
version: "2.5.4+6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.2.1+42
version: 2.2.1+44
environment:
sdk: ^3.5.4
@ -115,7 +115,7 @@ dependencies:
flutter_webrtc: ^0.12.5+hotfix.1
slide_countdown: ^2.0.2
video_compress: ^3.1.3
extended_image: ^9.0.9
cached_network_image: ^3.4.1
dev_dependencies:
flutter_test: