Compare commits

...

4 Commits

Author SHA1 Message Date
e236b7f98b 💄 Optimize attachment list width in post 2024-12-29 22:34:17 +08:00
5c7929e618 Post editor on device draft 2024-12-29 22:27:07 +08:00
7ba5260246 Improve image loading 2024-12-29 15:30:31 +08:00
a6d4947a23 🐛 Fix attachment list NaN height 2024-12-29 14:03:19 +08:00
14 changed files with 263 additions and 207 deletions

View File

@ -467,6 +467,7 @@
"accountStatusLastSeen": "Last seen at {}", "accountStatusLastSeen": "Last seen at {}",
"postArticle": "Article on the Solar Network", "postArticle": "Article on the Solar Network",
"postStory": "Story on the Solar Network", "postStory": "Story on the Solar Network",
"postLocalDraftRestored": "Restored from device",
"articleWrittenAt": "Written at {}", "articleWrittenAt": "Written at {}",
"articleEditedAt": "Edited at {}", "articleEditedAt": "Edited at {}",
"attachmentSaved": "Saved to album", "attachmentSaved": "Saved to album",

View File

@ -465,6 +465,7 @@
"accountStatusLastSeen": "最后一次上线于 {}", "accountStatusLastSeen": "最后一次上线于 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "从本地恢复草稿",
"articleWrittenAt": "发表于 {}", "articleWrittenAt": "发表于 {}",
"articleEditedAt": "编辑于 {}", "articleEditedAt": "编辑于 {}",
"attachmentSaved": "已保存到相册", "attachmentSaved": "已保存到相册",

View File

@ -456,6 +456,7 @@
"accountJoinedAt": "加入於 {}", "accountJoinedAt": "加入於 {}",
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
@ -464,6 +465,7 @@
"accountStatusLastSeen": "最後一次上線於 {}", "accountStatusLastSeen": "最後一次上線於 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "從本地恢復草稿",
"articleWrittenAt": "發表於 {}", "articleWrittenAt": "發表於 {}",
"articleEditedAt": "編輯於 {}", "articleEditedAt": "編輯於 {}",
"attachmentSaved": "已保存到相冊", "attachmentSaved": "已保存到相冊",

View File

@ -456,6 +456,7 @@
"accountJoinedAt": "加入於 {}", "accountJoinedAt": "加入於 {}",
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
@ -464,6 +465,7 @@
"accountStatusLastSeen": "最後一次上線於 {}", "accountStatusLastSeen": "最後一次上線於 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "從本地恢復草稿",
"articleWrittenAt": "發表於 {}", "articleWrittenAt": "發表於 {}",
"articleEditedAt": "編輯於 {}", "articleEditedAt": "編輯於 {}",
"attachmentSaved": "已保存到相冊", "attachmentSaved": "已保存到相冊",

View File

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

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
@ -8,6 +11,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -151,8 +155,18 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController aliasController = TextEditingController(); final TextEditingController aliasController = TextEditingController();
PostWriteController() { PostWriteController() {
titleController.addListener(() => notifyListeners()); titleController.addListener(() {
descriptionController.addListener(() => notifyListeners()); _temporaryPlanSave();
notifyListeners();
});
descriptionController.addListener(() {
_temporaryPlanSave();
notifyListeners();
});
contentController.addListener(() {
_temporaryPlanSave();
});
_temporaryLoad();
} }
String mode = kTitleMap.keys.first; String mode = kTitleMap.keys.first;
@ -298,6 +312,81 @@ class PostWriteController extends ChangeNotifier {
return compressedAttachment; return compressedAttachment;
} }
static const kTemporaryStorageKey = 'int_draft_post';
Timer? _temporarySaveTimer;
void _temporaryPlanSave() {
_temporarySaveTimer?.cancel();
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
_temporarySave();
log("[PostWriter] Temporary save saved.");
});
}
void _temporarySave() {
SharedPreferences.getInstance().then((prefs) {
if (titleController.text.isEmpty &&
descriptionController.text.isEmpty &&
contentController.text.isEmpty &&
thumbnail == null &&
attachments.isEmpty) {
prefs.remove(kTemporaryStorageKey);
return;
}
prefs.setString(
kTemporaryStorageKey,
jsonEncode({
'publisher': publisher,
'content': contentController.text,
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
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(),
'visibility': visibility,
'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers,
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
}),
);
});
}
bool temporaryRestored = false;
void _temporaryLoad() {
SharedPreferences.getInstance().then((prefs) {
final raw = prefs.getString(kTemporaryStorageKey);
if (raw == null) return;
final data = jsonDecode(raw);
contentController.text = data['content'];
aliasController.text = data['alias'] ?? '';
titleController.text = data['title'] ?? '';
descriptionController.text = data['description'] ?? '';
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
attachments
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
tags = List.from(data['tags'].map((ele) => ele['alias']));
categories = List.from(data['categories'].map((ele) => ele['alias']));
visibility = data['visibility'];
visibleUsers = List.from(data['visible_users_list'] ?? []);
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
temporaryRestored = true;
notifyListeners();
});
}
Future<void> uploadSingleAttachment(BuildContext context, int idx) async { Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
if (isBusy) return; if (isBusy) return;
@ -354,9 +443,11 @@ class PostWriteController extends ChangeNotifier {
); );
try { try {
final compressedAttachment = await _tryCompressVideoCopy(context, media); if (context.mounted) {
if (compressedAttachment != null) { final compressedAttachment = await _tryCompressVideoCopy(context, media);
item = await attach.updateOne(item, compressedId: compressedAttachment.id); if (compressedAttachment != null) {
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
}
} }
} catch (err) { } catch (err) {
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -415,6 +506,7 @@ class PostWriteController extends ChangeNotifier {
method: editingPost != null ? 'PUT' : 'POST', method: editingPost != null ? 'PUT' : 'POST',
), ),
); );
reset();
} catch (err) { } catch (err) {
if (!context.mounted) return; if (!context.mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -463,56 +555,67 @@ class PostWriteController extends ChangeNotifier {
void setPublisher(SnPublisher? item) { void setPublisher(SnPublisher? item) {
publisher = item; publisher = item;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setPublishedAt(DateTime? value) { void setPublishedAt(DateTime? value) {
publishedAt = value; publishedAt = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setPublishedUntil(DateTime? value) { void setPublishedUntil(DateTime? value) {
publishedUntil = value; publishedUntil = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setTags(List<String> value) { void setTags(List<String> value) {
tags = value; tags = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setCategories(List<String> value) { void setCategories(List<String> value) {
categories = value; categories = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setVisibility(int value) { void setVisibility(int value) {
visibility = value; visibility = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setVisibleUsers(List<int> value) { void setVisibleUsers(List<int> value) {
visibleUsers = value; visibleUsers = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setInvisibleUsers(List<int> value) { void setInvisibleUsers(List<int> value) {
invisibleUsers = value; invisibleUsers = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setProgress(double? value) { void setProgress(double? value) {
progress = value; progress = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setIsBusy(bool value) { void setIsBusy(bool value) {
isBusy = value; isBusy = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setMode(String value) { void setMode(String value) {
mode = value; mode = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
@ -530,6 +633,8 @@ class PostWriteController extends ChangeNotifier {
replyingPost = null; replyingPost = null;
repostingPost = null; repostingPost = null;
mode = kTitleMap.keys.first; mode = kTitleMap.keys.first;
temporaryRestored = false;
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
notifyListeners(); notifyListeners();
} }

View File

@ -364,6 +364,35 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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();
},
),
],
)
: const SizedBox.shrink(),
)
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
LoadingIndicator(isActive: _isLoading), LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null) if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(

View File

@ -18,7 +18,8 @@ class AttachmentList extends StatefulWidget {
final bool noGrow; final bool noGrow;
final BoxFit fit; final BoxFit fit;
final double? maxHeight; final double? maxHeight;
final EdgeInsets? listPadding; final double? minWidth;
final EdgeInsets? padding;
const AttachmentList({ const AttachmentList({
super.key, super.key,
@ -28,7 +29,8 @@ class AttachmentList extends StatefulWidget {
this.noGrow = false, this.noGrow = false,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.maxHeight, this.maxHeight,
this.listPadding, this.minWidth,
this.padding,
}); });
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8)); static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
@ -43,8 +45,6 @@ class _AttachmentListState extends State<AttachmentList> {
(_) => const Uuid().v4(), (_) => const Uuid().v4(),
); );
static const double kAttachmentMaxWidth = 640;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
@ -53,8 +53,8 @@ class _AttachmentListState extends State<AttachmentList> {
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none; widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer; final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final constraints = BoxConstraints( final constraints = BoxConstraints(
minWidth: 80, minWidth: widget.minWidth ?? 80,
maxHeight: widget.maxHeight ?? double.infinity, maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
); );
if (widget.data.isEmpty) return const SizedBox.shrink(); if (widget.data.isEmpty) return const SizedBox.shrink();
@ -67,50 +67,47 @@ class _AttachmentListState extends State<AttachmentList> {
} }
.toDouble(); .toDouble();
return Padding( return Container(
padding: widget.listPadding ?? EdgeInsets.zero, padding: widget.padding ?? EdgeInsets.zero,
child: Container( constraints: constraints,
constraints: constraints, child: GestureDetector(
width: double.infinity, child: AspectRatio(
child: GestureDetector( aspectRatio: singleAspectRatio,
child: AspectRatio( child: Container(
aspectRatio: singleAspectRatio, decoration: BoxDecoration(
child: Container( color: backgroundColor,
decoration: BoxDecoration( border: Border.fromBorderSide(borderSide),
color: backgroundColor, borderRadius: AttachmentList.kDefaultRadius,
border: Border.fromBorderSide(borderSide), ),
borderRadius: AttachmentList.kDefaultRadius, child: ClipRRect(
), borderRadius: AttachmentList.kDefaultRadius,
child: ClipRRect( child: AttachmentItem(
borderRadius: AttachmentList.kDefaultRadius, data: widget.data[0],
child: AttachmentItem( heroTag: heroTags[0],
data: widget.data[0], fit: widget.fit,
heroTag: heroTags[0],
fit: widget.fit,
),
), ),
), ),
), ),
onTap: () {
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: 0,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
), ),
onTap: () {
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: 0,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
), ),
); );
} }
if (widget.gridded) { if (widget.gridded) {
return Padding( return Padding(
padding: widget.listPadding ?? EdgeInsets.zero, padding: widget.padding ?? EdgeInsets.zero,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
@ -134,7 +131,7 @@ class _AttachmentListState extends State<AttachmentList> {
child: AttachmentItem( child: AttachmentItem(
data: ele, data: ele,
heroTag: heroTags[idx], heroTag: heroTags[idx],
fit: widget.fit, fit: BoxFit.cover,
), ),
), ),
onTap: () { onTap: () {
@ -220,7 +217,7 @@ class _AttachmentListState extends State<AttachmentList> {
); );
}, },
separatorBuilder: (context, index) => const Gap(8), separatorBuilder: (context, index) => const Gap(8),
padding: widget.listPadding, padding: widget.padding,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
), ),

View File

@ -162,7 +162,7 @@ class ChatMessage extends StatelessWidget {
gridded: true, gridded: true,
noGrow: true, noGrow: true,
maxHeight: 520, maxHeight: 520,
listPadding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(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(6),
], ],

View File

@ -253,8 +253,9 @@ class PostItem extends StatelessWidget {
bordered: true, bordered: true,
gridded: true, gridded: true,
maxHeight: showFullPost ? null : 480, maxHeight: showFullPost ? null : 480,
minWidth: 640,
fit: showFullPost ? BoxFit.cover : BoxFit.contain, fit: showFullPost ? BoxFit.cover : BoxFit.contain,
listPadding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
if (data.body['content'] != null) if (data.body['content'] != null)
LinkPreviewWidget( LinkPreviewWidget(
@ -336,10 +337,10 @@ class PostShareImageWidget extends StatelessWidget {
isRelativeDate: false, isRelativeDate: false,
).padding(horizontal: 16, bottom: 8), ).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
gridded: true, gridded: true,
).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -930,9 +931,10 @@ class _PostQuoteContent extends StatelessWidget {
child: AttachmentList( child: AttachmentList(
data: child.preload!.attachments!, data: child.preload!.attachments!,
maxHeight: 360, maxHeight: 360,
minWidth: 640,
fit: BoxFit.contain, fit: BoxFit.contain,
gridded: true, gridded: true,
listPadding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
).padding( ).padding(
top: 8, top: 8,

View File

@ -1,4 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -7,7 +7,6 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
// Keep this import to make the web image render work // 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'; import 'package:surface/providers/config.dart';
class UniversalImage extends StatelessWidget { class UniversalImage extends StatelessWidget {
@ -34,64 +33,54 @@ class UniversalImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final quality = filterQuality ?? context.read<ConfigProvider>().imageQuality;
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
return Image( return ExtendedImage.network(
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality, url,
image: kIsWeb
? UniversalImage.provider(url)
: ResizeImage(
UniversalImage.provider(url),
width: resizeWidth?.round(),
height: resizeHeight?.round(),
policy: ResizeImagePolicy.fit,
),
width: width, width: width,
height: height, height: height,
fit: fit, fit: fit,
loadingBuilder: noProgressIndicator cache: true,
? null compressionRatio: kIsWeb ? 1 : switch(quality) {
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { FilterQuality.high => 1,
if (loadingProgress == null) return child; FilterQuality.medium => 0.75,
return Center( FilterQuality.low => 0.5,
child: TweenAnimationBuilder( FilterQuality.none => 0.25,
tween: Tween( },
begin: 0, filterQuality: quality,
end: loadingProgress.expectedTotalBytes != null enableLoadState: true,
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! retries: 3,
: 0, 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,
), ),
duration: const Duration(milliseconds: 300), ],
builder: (context, value, _) => CircularProgressIndicator( ).center(),
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, ),
), );
), }
); return Center(
}, child: CircularProgressIndicator(
errorBuilder: noErrorWidget value: state.loadingProgress != null
? null ? state.loadingProgress!.cumulativeBytesLoaded / state.loadingProgress!.expectedTotalBytes!
: (context, error, stackTrace) { : null,
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(),
),
);
},
); );
} }
@ -99,9 +88,10 @@ class UniversalImage extends StatelessWidget {
// This place used to use network image or cached network image depending on the platform. // 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. // But now the cached network image is working on every platform.
// So we just use it now. // So we just use it now.
return CachedNetworkImageProvider( return ExtendedNetworkImageProvider(
url, url,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, cache: true,
retries: 3,
); );
} }
} }

View File

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

View File

@ -182,30 +182,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.9.3" 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: cassowary:
dependency: transitive dependency: transitive
description: description:
@ -454,6 +430,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.7" 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: fading_edge_scrollview:
dependency: transitive dependency: transitive
description: description:
@ -643,14 +635,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.2" 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: flutter_colorpicker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -890,6 +874,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.2" 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: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -1250,14 +1242,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" 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: package_config:
dependency: transitive dependency: transitive
description: description:
@ -1546,14 +1530,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" 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: safe_local_storage:
dependency: transitive dependency: transitive
description: description:
@ -1767,46 +1743,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" 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: stack_trace:
dependency: transitive dependency: transitive
description: description:

View File

@ -53,7 +53,6 @@ dependencies:
markdown: ^7.2.2 markdown: ^7.2.2
flutter_markdown: ^0.7.4+1 flutter_markdown: ^0.7.4+1
url_launcher: ^6.3.1 url_launcher: ^6.3.1
cached_network_image: ^3.4.1
flutter_animate: ^4.5.0 flutter_animate: ^4.5.0
syntax_highlight: ^0.4.0 syntax_highlight: ^0.4.0
google_fonts: ^6.2.1 google_fonts: ^6.2.1
@ -116,6 +115,7 @@ dependencies:
flutter_webrtc: ^0.12.5+hotfix.1 flutter_webrtc: ^0.12.5+hotfix.1
slide_countdown: ^2.0.2 slide_countdown: ^2.0.2
video_compress: ^3.1.3 video_compress: ^3.1.3
extended_image: ^9.0.9
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: