Compare commits

..

14 Commits

Author SHA1 Message Date
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
96fd64d85d 🚀 Launch 2.2.1+42 2024-12-29 22:43:58 +08:00
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
7fbd4e9647 🚀 Launch 2.2.1+41 2024-12-29 12:15:22 +08:00
95d926b29f Bug fixes 2024-12-29 12:09:04 +08:00
f6cf6d0440 💄 Experimental new attachment layout 2024-12-29 12:02:26 +08:00
e503c3f02f Use analyze now for images 2024-12-29 11:09:54 +08:00
19 changed files with 466 additions and 182 deletions

View File

@ -282,6 +282,7 @@
"other": "{} attachments" "other": "{} attachments"
}, },
"fieldAttachmentRandomId": "Random ID", "fieldAttachmentRandomId": "Random ID",
"fieldAttachmentAlt": "Alternative text",
"addAttachmentFromAlbum": "Add from album", "addAttachmentFromAlbum": "Add from album",
"addAttachmentFromClipboard": "Paste file", "addAttachmentFromClipboard": "Paste file",
"addAttachmentFromCameraPhoto": "Take photo", "addAttachmentFromCameraPhoto": "Take photo",
@ -293,6 +294,7 @@
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail", "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
"attachmentCompressVideo": "Re-encode video", "attachmentCompressVideo": "Re-encode video",
"attachmentSetThumbnail": "Set thumbnail", "attachmentSetThumbnail": "Set thumbnail",
"attachmentSetAlt": "Set alternative text",
"attachmentCopyRandomId": "Copy RID", "attachmentCopyRandomId": "Copy RID",
"attachmentUpload": "Upload", "attachmentUpload": "Upload",
"attachmentInputDialog": "Upload attachments", "attachmentInputDialog": "Upload attachments",
@ -458,6 +460,7 @@
"accountJoinedAt": "Joined at {}", "accountJoinedAt": "Joined at {}",
"accountBirthday": "Born on {}", "accountBirthday": "Born on {}",
"accountBadge": "Badge", "accountBadge": "Badge",
"accountCheckInNoRecords": "No check-in records",
"badgeCompanyStaff": "Solsynth Staff", "badgeCompanyStaff": "Solsynth Staff",
"badgeSiteMigration": "Solar Network Native", "badgeSiteMigration": "Solar Network Native",
"accountStatus": "Status", "accountStatus": "Status",
@ -466,6 +469,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

@ -280,6 +280,7 @@
"other": "{} 个附件" "other": "{} 个附件"
}, },
"fieldAttachmentRandomId": "访问 ID", "fieldAttachmentRandomId": "访问 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "从相册中添加附件", "addAttachmentFromAlbum": "从相册中添加附件",
"addAttachmentFromClipboard": "粘贴附件", "addAttachmentFromClipboard": "粘贴附件",
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
@ -291,6 +292,7 @@
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图", "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
"attachmentCompressVideo": "重新编码视频", "attachmentCompressVideo": "重新编码视频",
"attachmentSetThumbnail": "设置缩略图", "attachmentSetThumbnail": "设置缩略图",
"attachmentSetAlt": "设置概述文字",
"attachmentCopyRandomId": "复制访问 ID", "attachmentCopyRandomId": "复制访问 ID",
"attachmentUpload": "上传", "attachmentUpload": "上传",
"attachmentInputDialog": "上传附件", "attachmentInputDialog": "上传附件",
@ -456,6 +458,7 @@
"accountJoinedAt": "加入于 {}", "accountJoinedAt": "加入于 {}",
"accountBirthday": "出生于 {}", "accountBirthday": "出生于 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暂无运势记录",
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工", "badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "状态", "accountStatus": "状态",
@ -464,6 +467,7 @@
"accountStatusLastSeen": "最后一次上线于 {}", "accountStatusLastSeen": "最后一次上线于 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "从本地恢复草稿",
"articleWrittenAt": "发表于 {}", "articleWrittenAt": "发表于 {}",
"articleEditedAt": "编辑于 {}", "articleEditedAt": "编辑于 {}",
"attachmentSaved": "已保存到相册", "attachmentSaved": "已保存到相册",

View File

@ -280,6 +280,7 @@
"other": "{} 個附件" "other": "{} 個附件"
}, },
"fieldAttachmentRandomId": "訪問 ID", "fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件", "addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +292,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻", "attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖", "attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID", "attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳", "attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件", "attachmentInputDialog": "上傳附件",
@ -456,6 +458,7 @@
"accountJoinedAt": "加入於 {}", "accountJoinedAt": "加入於 {}",
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
@ -464,6 +467,7 @@
"accountStatusLastSeen": "最後一次上線於 {}", "accountStatusLastSeen": "最後一次上線於 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "從本地恢復草稿",
"articleWrittenAt": "發表於 {}", "articleWrittenAt": "發表於 {}",
"articleEditedAt": "編輯於 {}", "articleEditedAt": "編輯於 {}",
"attachmentSaved": "已保存到相冊", "attachmentSaved": "已保存到相冊",

View File

@ -280,6 +280,7 @@
"other": "{} 個附件" "other": "{} 個附件"
}, },
"fieldAttachmentRandomId": "訪問 ID", "fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件", "addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +292,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻", "attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖", "attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID", "attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳", "attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件", "attachmentInputDialog": "上傳附件",
@ -456,6 +458,7 @@
"accountJoinedAt": "加入於 {}", "accountJoinedAt": "加入於 {}",
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
@ -464,6 +467,7 @@
"accountStatusLastSeen": "最後一次上線於 {}", "accountStatusLastSeen": "最後一次上線於 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "從本地恢復草稿",
"articleWrittenAt": "發表於 {}", "articleWrittenAt": "發表於 {}",
"articleEditedAt": "編輯於 {}", "articleEditedAt": "編輯於 {}",
"attachmentSaved": "已保存到相冊", "attachmentSaved": "已保存到相冊",

View File

@ -173,7 +173,7 @@ PODS:
- in_app_review (2.0.0): - in_app_review (2.0.0):
- Flutter - Flutter
- Kingfisher (8.1.3) - Kingfisher (8.1.3)
- livekit_client (2.3.3): - livekit_client (2.3.4):
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@ -391,7 +391,7 @@ SPEC CHECKSUMS:
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e

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;
@ -199,11 +213,11 @@ class PostWriteController extends ChangeNotifier {
aliasController.text = post.alias ?? ''; aliasController.text = post.alias ?? '';
publishedAt = post.publishedAt; publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil; publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? []); visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
invisibleUsers = List.from(post.invisibleUsersList ?? []); invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
visibility = post.visibility; visibility = post.visibility;
tags = List.from(post.tags.map((ele) => ele.alias)); tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
categories = List.from(post.categories.map((ele) => ele.alias)); categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
@ -247,6 +261,7 @@ class PostWriteController extends ChangeNotifier {
media.toFile()!, media.toFile()!,
place.$1, place.$1,
place.$2, place.$2,
analyzeNow: media.type == SnMediaType.image,
onProgress: (value) { onProgress: (value) {
progress = value; progress = value;
notifyListeners(); notifyListeners();
@ -297,6 +312,82 @@ 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(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,
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;
@ -353,9 +444,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);
@ -414,6 +507,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);
@ -462,56 +556,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();
} }
@ -529,6 +634,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

@ -154,10 +154,12 @@ class SnAttachmentProvider {
String rid, String rid,
String cid, { String cid, {
Function(double progress)? onProgress, Function(double progress)? onProgress,
bool analyzeNow = false,
}) async { }) async {
final resp = await _sn.client.post( final resp = await _sn.client.post(
'/cgi/uc/fragments/$rid/$cid', '/cgi/uc/fragments/$rid/$cid',
data: data, data: data,
queryParameters: {'analyzeNow': analyzeNow},
options: Options(headers: {'Content-Type': 'application/octet-stream'}), options: Options(headers: {'Content-Type': 'application/octet-stream'}),
onSendProgress: (count, total) { onSendProgress: (count, total) {
if (onProgress != null) { if (onProgress != null) {
@ -178,6 +180,7 @@ class SnAttachmentProvider {
SnAttachmentFragment place, SnAttachmentFragment place,
int chunkSize, { int chunkSize, {
Function(double progress)? onProgress, Function(double progress)? onProgress,
bool analyzeNow = false,
}) async { }) async {
final Map<String, dynamic> chunks = place.fileChunks; final Map<String, dynamic> chunks = place.fileChunks;
var completedTasks = 0; var completedTasks = 0;
@ -200,6 +203,7 @@ class SnAttachmentProvider {
data, data,
place.rid, place.rid,
entry.key, entry.key,
analyzeNow: analyzeNow,
onProgress: (progress) { onProgress: (progress) {
final overallProgress = (completedTasks + progress) / chunks.length; final overallProgress = (completedTasks + progress) / chunks.length;
onProgress?.call(overallProgress); onProgress?.call(overallProgress);

View File

@ -517,6 +517,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
future: _getCheckInRecords(), future: _getCheckInRecords(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink(); if (!snapshot.hasData) return const SizedBox.shrink();
if (snapshot.data!.length <= 1) {
return Text(
'accountCheckInNoRecords',
textAlign: TextAlign.center,
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
}
final records = snapshot.data!; final records = snapshot.data!;
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,

View File

@ -364,6 +364,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container(
child: _writeController.temporaryRestored
? 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)
.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

@ -20,9 +20,11 @@ import 'package:uuid/uuid.dart';
class AttachmentItem extends StatelessWidget { class AttachmentItem extends StatelessWidget {
final SnAttachment? data; final SnAttachment? data;
final String? heroTag; final String? heroTag;
final BoxFit fit;
const AttachmentItem({ const AttachmentItem({
super.key, super.key,
this.fit = BoxFit.cover,
required this.data, required this.data,
required this.heroTag, required this.heroTag,
}); });
@ -43,7 +45,7 @@ class AttachmentItem extends StatelessWidget {
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data!.rid), sn.getAttachmentUrl(data!.rid),
key: Key('attachment-${data!.rid}-$tag'), key: Key('attachment-${data!.rid}-$tag'),
fit: BoxFit.cover, fit: fit,
), ),
); );
case 'video': case 'video':
@ -313,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
} }
return MaterialDesktopVideoControlsTheme( return MaterialDesktopVideoControlsTheme(
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
normal: MaterialDesktopVideoControlsThemeData( normal: MaterialDesktopVideoControlsThemeData(
buttonBarButtonSize: 24, buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white, buttonBarButtonColor: Colors.white,
@ -322,12 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
MaterialDesktopCustomButton( MaterialDesktopCustomButton(
iconSize: 24, iconSize: 24,
onPressed: _toggleOriginal, onPressed: _toggleOriginal,
icon: _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(), fullscreen: const MaterialDesktopVideoControlsThemeData(),
child: MaterialVideoControlsTheme( child: MaterialVideoControlsTheme(
key: Key('material-video-controls-theme-$_showOriginal'),
normal: MaterialVideoControlsThemeData( normal: MaterialVideoControlsThemeData(
buttonBarButtonSize: 24, buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white, buttonBarButtonColor: Colors.white,

View File

@ -4,8 +4,8 @@ import 'package:collection/collection.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
@ -14,19 +14,23 @@ import 'package:uuid/uuid.dart';
class AttachmentList extends StatefulWidget { class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data; final List<SnAttachment?> data;
final bool bordered; final bool bordered;
final bool gridded;
final bool noGrow; final bool noGrow;
final bool isFlatted; 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,
required this.data, required this.data,
this.bordered = false, this.bordered = false,
this.gridded = false,
this.noGrow = false, this.noGrow = false,
this.isFlatted = false, 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));
@ -41,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(
@ -51,9 +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,
maxWidth: layoutConstraints.maxWidth - 20,
); );
if (widget.data.isEmpty) return const SizedBox.shrink(); if (widget.data.isEmpty) return const SizedBox.shrink();
@ -67,97 +68,90 @@ class _AttachmentListState extends State<AttachmentList> {
.toDouble(); .toDouble();
return Container( return Container(
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE) padding: widget.padding ?? EdgeInsets.zero,
? constraints.copyWith( constraints: constraints,
maxWidth: math.min( child: GestureDetector(
constraints.maxWidth, child: AspectRatio(
kAttachmentMaxWidth, aspectRatio: singleAspectRatio,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border.fromBorderSide(borderSide),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[0],
heroTag: heroTags[0],
fit: widget.fit,
), ),
) ),
: null,
child: AspectRatio(
aspectRatio: singleAspectRatio,
child: GestureDetector(
child: Builder(
builder: (context) {
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
return Padding(
// Single child list-like displaying
padding: widget.listPadding ?? EdgeInsets.zero,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(top: borderSide, bottom: borderSide),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[0],
heroTag: heroTags[0],
),
),
),
);
}
return Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(top: borderSide, bottom: borderSide),
),
child: AttachmentItem(
data: widget.data[0],
heroTag: heroTags.first,
),
);
},
), ),
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.isFlatted) { if (widget.gridded) {
return Wrap( return Padding(
spacing: 4, padding: widget.padding ?? EdgeInsets.zero,
runSpacing: 4, child: Container(
children: widget.data decoration: BoxDecoration(
.mapIndexed( color: backgroundColor,
(idx, ele) => AspectRatio( border: Border(
aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(), top: borderSide,
child: Container( bottom: borderSide,
decoration: BoxDecoration( ),
color: backgroundColor, borderRadius: AttachmentList.kDefaultRadius,
border: Border( ),
top: borderSide, child: ClipRRect(
bottom: borderSide, 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,
);
},
), ),
borderRadius: AttachmentList.kDefaultRadius, )
), .toList(),
child: ClipRRect( ),
borderRadius: AttachmentList.kDefaultRadius, ),
child: AttachmentItem( ),
data: ele,
heroTag: heroTags[idx],
),
),
),
),
)
.toList(),
); );
} }
@ -223,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

@ -0,0 +1,87 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.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

@ -53,7 +53,7 @@ class ChatMessage extends StatelessWidget {
iconOnRightSwipe: Symbols.edit, iconOnRightSwipe: Symbols.edit,
swipeSensitivity: 20, swipeSensitivity: 20,
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null, onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null, onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
child: ContextMenuArea( child: ContextMenuArea(
contextMenu: ContextMenu( contextMenu: ContextMenu(
entries: [ entries: [
@ -103,18 +103,17 @@ class ChatMessage extends StatelessWidget {
children: [ children: [
if (!isMerged) if (!isMerged)
Row( Row(
crossAxisAlignment: CrossAxisAlignment.baseline, crossAxisAlignment: CrossAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
children: [ children: [
if (isCompact) if (isCompact)
AccountImage( AccountImage(
content: user?.avatar, content: user?.avatar,
radius: 12, radius: 12,
).padding(right: 6), ).padding(right: 8),
Text( Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown', (data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(), ).bold(),
const Gap(6), const Gap(8),
Text( Text(
dateFormatter.format(data.createdAt.toLocal()), dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13), ).fontSize(13),
@ -153,15 +152,17 @@ class ChatMessage extends StatelessWidget {
) )
], ],
).opacity(isPending ? 0.5 : 1), ).opacity(isPending ? 0.5 : 1),
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false)) if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
LinkPreviewWidget(text: data.body['text']!), LinkPreviewWidget(text: data.body['text']!),
if (data.preload?.attachments?.isNotEmpty ?? false) if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
gridded: true,
noGrow: true, noGrow: true,
maxHeight: 520, maxHeight: 560,
listPadding: const EdgeInsets.only(top: 8), minWidth: 480,
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

@ -83,6 +83,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
media.toFile()!, media.toFile()!,
place.$1, place.$1,
place.$2, place.$2,
analyzeNow: media.type == SnMediaType.image,
onProgress: (progress) { onProgress: (progress) {
// Calculate overall progress for attachments // Calculate overall progress for attachments
setState(() { setState(() {
@ -160,75 +161,84 @@ class ChatMessageInputState extends State<ChatMessageInput> {
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: Padding( child: _replyingMessage != null
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero, ? Container(
child: _replyingMessage != null padding: const EdgeInsets.only(left: 16, right: 16),
? MaterialBanner( decoration: BoxDecoration(
padding: const EdgeInsets.only(left: 16.0), borderRadius: const BorderRadius.all(Radius.circular(8)),
leading: const Icon(Symbols.reply), border: Border(
backgroundColor: Colors.transparent, bottom: BorderSide(
content: SingleChildScrollView( color: Theme.of(context).dividerColor,
physics: const NeverScrollableScrollPhysics(), width: 1 / MediaQuery.of(context).devicePixelRatio,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_replyingMessage?.body['text'] != null)
MarkdownTextContent(
content: _replyingMessage?.body['text'],
),
],
), ),
), ),
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()), child: Text('cancel'.tr()),
onPressed: () { onTap: () {
setState(() => _replyingMessage = null); setState(() => _replyingMessage = null);
}, },
), ),
], ],
) ).padding(vertical: 8),
: const SizedBox.shrink(), )
), : 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), .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: Padding( child: _editingMessage != null
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero, ? Container(
child: _editingMessage != null padding: const EdgeInsets.only(left: 16, right: 16),
? MaterialBanner( decoration: BoxDecoration(
padding: const EdgeInsets.only(left: 16.0), borderRadius: const BorderRadius.all(Radius.circular(8)),
leading: const Icon(Symbols.edit), border: Border(
backgroundColor: Colors.transparent, bottom: BorderSide(
content: SingleChildScrollView( color: Theme.of(context).dividerColor,
physics: const NeverScrollableScrollPhysics(), width: 1 / MediaQuery.of(context).devicePixelRatio,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_editingMessage?.body['text'] != null)
MarkdownTextContent(
content: _editingMessage?.body['text'],
),
],
), ),
), ),
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()), child: Text('cancel'.tr()),
onPressed: () { onTap: () {
_contentController.clear();
setState(() => _editingMessage = null); setState(() => _editingMessage = null);
}, },
), ),
], ],
) ).padding(vertical: 8),
: const SizedBox.shrink(), )
), : 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), .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SizedBox( SizedBox(
height: 56, height: 56,

View File

@ -251,8 +251,11 @@ class PostItem extends StatelessWidget {
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
maxHeight: 560, gridded: true,
listPadding: const EdgeInsets.symmetric(horizontal: 12), maxHeight: showFullPost ? null : 480,
minWidth: 640,
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
if (data.body['content'] != null) if (data.body['content'] != null)
LinkPreviewWidget( LinkPreviewWidget(
@ -332,13 +335,12 @@ class PostShareImageWidget extends StatelessWidget {
_PostQuoteContent( _PostQuoteContent(
child: data.repostTo!, child: data.repostTo!,
isRelativeDate: false, isRelativeDate: false,
isFlatted: true,
).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!,
isFlatted: true, gridded: true,
).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -884,11 +886,9 @@ class _PostContentBody extends StatelessWidget {
class _PostQuoteContent extends StatelessWidget { class _PostQuoteContent extends StatelessWidget {
final SnPost child; final SnPost child;
final bool isRelativeDate; final bool isRelativeDate;
final bool isFlatted;
const _PostQuoteContent({ const _PostQuoteContent({
this.isRelativeDate = true, this.isRelativeDate = true,
this.isFlatted = false,
required this.child, required this.child,
}); });
@ -930,12 +930,15 @@ class _PostQuoteContent extends StatelessWidget {
), ),
child: AttachmentList( child: AttachmentList(
data: child.preload!.attachments!, data: child.preload!.attachments!,
isFlatted: isFlatted, maxHeight: 360,
listPadding: const EdgeInsets.symmetric(horizontal: 12), minWidth: 640,
fit: BoxFit.contain,
gridded: true,
padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
).padding( ).padding(
top: 8, top: 8,
bottom: (child.preload?.attachments?.length ?? 0) > 1 ? 12 : 0, bottom: 12,
) )
else else
const Gap(8), const Gap(8),

View File

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

View File

@ -134,7 +134,7 @@ PODS:
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- in_app_review (2.0.0): - in_app_review (2.0.0):
- FlutterMacOS - FlutterMacOS
- livekit_client (2.3.3): - livekit_client (2.3.4):
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@ -304,7 +304,7 @@ SPEC CHECKSUMS:
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: 8b1b90a6f2445d127a018ce93cc8cf6d8ab62982 livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5

View File

@ -1086,10 +1086,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: a3ff529fe6745ee40cdedcd021d81c4a6ad946dd495e782596f2856eeeabc739 sha256: "7cdeb3eaeec7fb70a4cf88d9caabccbef9e3bd5f0b23c086320bc5c9acb2770b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.3" version: "2.3.4"
logging: logging:
dependency: transitive dependency: transitive
description: 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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 2.2.1+40 version: 2.2.1+43
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -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
cached_network_image: ^3.4.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: