Compare commits

..

11 Commits

Author SHA1 Message Date
LittleSheep
7fbd4e9647 🚀 Launch 2.2.1+41 2024-12-29 12:15:22 +08:00
LittleSheep
95d926b29f Bug fixes 2024-12-29 12:09:04 +08:00
LittleSheep
f6cf6d0440 💄 Experimental new attachment layout 2024-12-29 12:02:26 +08:00
LittleSheep
e503c3f02f Use analyze now for images 2024-12-29 11:09:54 +08:00
LittleSheep
d4fbdd397e Create boost 2024-12-29 02:13:31 +08:00
LittleSheep
03943a7138 🗑️ Remove link expand from post share 2024-12-28 20:35:43 +08:00
LittleSheep
44f2c5fe0e Toggle original or compressed one via video control 2024-12-28 19:59:04 +08:00
LittleSheep
bb66d5b684 Able to upload low quality video copy 2024-12-28 19:23:49 +08:00
LittleSheep
1fca36293d 🐛 Fix attachment set thumbnail 2024-12-28 18:16:59 +08:00
LittleSheep
2c7dc8c2ea 🐛 Fix attachment uploading progress 2024-12-28 17:37:58 +08:00
LittleSheep
cf0df91d8c 👽 Fix attachment uploading 2024-12-28 17:19:20 +08:00
23 changed files with 2159 additions and 379 deletions

View File

@@ -0,0 +1,30 @@
meta {
name: Activate Boost
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/uc/boosts/1/activate
body: none
auth: bearer
}
auth:bearer {
token: {{atk}}
}
body:json {
{
"client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}",
"type": "general",
"subject": "Merry Christmas!",
"subtitle": "一条来自 Solar Network 团队的信息",
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
"metadata": {
"image": "6EqsYQwmFRCkbmhR"
},
"priority": 10
}
}

View File

@@ -298,6 +298,26 @@
"attachmentInputDialog": "Upload attachments", "attachmentInputDialog": "Upload attachments",
"attachmentInputUseRandomId": "Use Random ID", "attachmentInputUseRandomId": "Use Random ID",
"attachmentInputNew": "New Upload", "attachmentInputNew": "New Upload",
"waitingForUpload": "Waiting for upload",
"attachmentVideoCompressHint": "Compress a copy of this video",
"attachmentVideoCompressHintDescription": "Do you want to upload a compress copy of video {}? It will help your audience to preview this video faster and they still can watch the original video. It will take some while to process the video on your device, so please be patient.",
"attachmentCompressQuality": "Compress quality",
"attachmentCompressQualityHighest": "Highest",
"attachmentCompressQualityDefault": "Default",
"attachmentCompressQualityMedium": "Medium",
"attachmentCompressQualityLow": "Low",
"attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality.",
"attachmentUploaded": "Uploaded",
"attachmentPending": "Pending",
"attachmentCopyCompressed": "Copy compressed",
"attachmentGotBoosted": "Boosted",
"attachmentBoost": "Boost",
"attachmentCreateBoost": "Create Boost",
"attachmentBoostHint": "Boost is a feature that allows you to upload attachments to a server closer to your audience or a faster content network. This feature is currently in beta and is subject to change. It's all free for now, you can feel free to try, you will get notified when the pricing plan changed.",
"attachmentDestinationRegion": "Destination Region",
"attachmentDestinationRegionAPAC": "Asia Pacific",
"attachmentDestinationRegionNGB": "Ning Bo, China, Zhejiang",
"attachmentDestinationRegionHKG": "Hong Kong",
"notification": "Notification", "notification": "Notification",
"notificationUnreadCount": { "notificationUnreadCount": {
"zero": "All notifications read", "zero": "All notifications read",
@@ -438,6 +458,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",
@@ -513,12 +534,5 @@
"postCategoryKnowledge": "Knowledge", "postCategoryKnowledge": "Knowledge",
"postCategoryLiterature": "Literature", "postCategoryLiterature": "Literature",
"postCategoryFunny": "Funny", "postCategoryFunny": "Funny",
"postCategoryUncategorized": "Uncategorized", "postCategoryUncategorized": "Uncategorized"
"waitingForUpload": "Waiting for upload",
"attachmentCompressQuality": "Compress quality",
"attachmentCompressQualityHighest": "Highest",
"attachmentCompressQualityDefault": "Default",
"attachmentCompressQualityMedium": "Medium",
"attachmentCompressQualityLow": "Low",
"attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality."
} }

View File

@@ -296,6 +296,26 @@
"attachmentInputDialog": "上传附件", "attachmentInputDialog": "上传附件",
"attachmentInputUseRandomId": "使用访问 ID", "attachmentInputUseRandomId": "使用访问 ID",
"attachmentInputNew": "新上传附件", "attachmentInputNew": "新上传附件",
"waitingForUpload": "等待上传",
"attachmentVideoCompressHint": "压缩一份视频的副本",
"attachmentVideoCompressHintDescription": "你想上传压缩视频 {} 的副本吗?它将帮助你的观众快速预览视频,并且他们仍然可以观看原始视频。这将会在在你的设备上处理视频,所以需要一些时间,所以请耐心等待。",
"attachmentCompressQuality": "压缩质量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默认",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。",
"attachmentUploaded": "已上传",
"attachmentPending": "未上传",
"attachmentCopyCompressed": "有压缩副本",
"attachmentGotBoosted": "有加速传递",
"attachmentBoost": "加速包",
"attachmentCreateBoost": "加速传递",
"attachmentBoostHint": "加速传递允许您将附件上传到更近的受众或更快的内容网络。该功能目前处于 Beta 阶段。该功能限时免费,当有价格计划更改时,您将会被通知。",
"attachmentDestinationRegion": "目标节点",
"attachmentDestinationRegionAPAC": "亚太地区",
"attachmentDestinationRegionNGB": "中国 · 浙江 · 宁波",
"attachmentDestinationRegionHKG": "香港",
"notification": "通知", "notification": "通知",
"notificationUnreadCount": { "notificationUnreadCount": {
"zero": "无未读通知", "zero": "无未读通知",
@@ -436,6 +456,7 @@
"accountJoinedAt": "加入于 {}", "accountJoinedAt": "加入于 {}",
"accountBirthday": "出生于 {}", "accountBirthday": "出生于 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暂无运势记录",
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工", "badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "状态", "accountStatus": "状态",
@@ -511,12 +532,5 @@
"postCategoryKnowledge": "知识", "postCategoryKnowledge": "知识",
"postCategoryLiterature": "文学", "postCategoryLiterature": "文学",
"postCategoryFunny": "搞笑", "postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分类", "postCategoryUncategorized": "未分类"
"waitingForUpload": "等待上传",
"attachmentCompressQuality": "压缩质量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默认",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。"
} }

View File

@@ -296,6 +296,26 @@
"attachmentInputDialog": "上傳附件", "attachmentInputDialog": "上傳附件",
"attachmentInputUseRandomId": "使用訪問 ID", "attachmentInputUseRandomId": "使用訪問 ID",
"attachmentInputNew": "新上傳附件", "attachmentInputNew": "新上傳附件",
"waitingForUpload": "等待上傳",
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
"attachmentUploaded": "已上傳",
"attachmentPending": "未上傳",
"attachmentCopyCompressed": "有壓縮副本",
"attachmentGotBoosted": "有加速傳遞",
"attachmentBoost": "加速包",
"attachmentCreateBoost": "加速傳遞",
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
"attachmentDestinationRegion": "目標節點",
"attachmentDestinationRegionAPAC": "亞太地區",
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
"attachmentDestinationRegionHKG": "香港",
"notification": "通知", "notification": "通知",
"notificationUnreadCount": { "notificationUnreadCount": {
"zero": "無未讀通知", "zero": "無未讀通知",
@@ -511,12 +531,5 @@
"postCategoryKnowledge": "知識", "postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學", "postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑", "postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類", "postCategoryUncategorized": "未分類"
"waitingForUpload": "等待上傳",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
} }

View File

@@ -296,6 +296,26 @@
"attachmentInputDialog": "上傳附件", "attachmentInputDialog": "上傳附件",
"attachmentInputUseRandomId": "使用訪問 ID", "attachmentInputUseRandomId": "使用訪問 ID",
"attachmentInputNew": "新上傳附件", "attachmentInputNew": "新上傳附件",
"waitingForUpload": "等待上傳",
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
"attachmentUploaded": "已上傳",
"attachmentPending": "未上傳",
"attachmentCopyCompressed": "有壓縮副本",
"attachmentGotBoosted": "有加速傳遞",
"attachmentBoost": "加速包",
"attachmentCreateBoost": "加速傳遞",
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
"attachmentDestinationRegion": "目標節點",
"attachmentDestinationRegionAPAC": "亞太地區",
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
"attachmentDestinationRegionHKG": "香港",
"notification": "通知", "notification": "通知",
"notificationUnreadCount": { "notificationUnreadCount": {
"zero": "無未讀通知", "zero": "無未讀通知",
@@ -511,12 +531,5 @@
"postCategoryKnowledge": "知識", "postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學", "postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑", "postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類", "postCategoryUncategorized": "未分類"
"waitingForUpload": "等待上傳",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
} }

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

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@@ -14,6 +15,7 @@ import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:video_compress/video_compress.dart';
class PostWriteMedia { class PostWriteMedia {
late String name; late String name;
@@ -229,7 +231,8 @@ class PostWriteController extends ChangeNotifier {
} }
} }
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media) async { Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
{bool isCompressed = false}) async {
final attach = context.read<SnAttachmentProvider>(); final attach = context.read<SnAttachmentProvider>();
final place = await attach.chunkedUploadInitialize( final place = await attach.chunkedUploadInitialize(
@@ -240,19 +243,61 @@ class PostWriteController extends ChangeNotifier {
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
); );
final item = await attach.chunkedUploadParts( var item = await attach.chunkedUploadParts(
media.toFile()!, media.toFile()!,
place.$1, place.$1,
place.$2, place.$2,
onProgress: (progress) { analyzeNow: media.type == SnMediaType.image,
progress = progress; onProgress: (value) {
progress = value;
notifyListeners(); notifyListeners();
}, },
); );
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
try {
final compressedAttachment = await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) {
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
}
} catch (err) {
if (context.mounted) context.showErrorDialog(err);
}
}
return item; return item;
} }
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
if (media.type != SnMediaType.video) return null;
if (media.file == null) return null;
if (VideoCompress.isCompressing) return null;
final confirm = await context.showConfirmDialog(
'attachmentVideoCompressHint'.tr(),
'attachmentVideoCompressHintDescription'.tr(args: [media.file!.name]),
);
if (!confirm) return null;
progress = null;
notifyListeners();
final mediaInfo = await VideoCompress.compressVideo(
media.file!.path,
quality: VideoQuality.LowQuality,
frameRate: 30,
deleteOrigin: false,
);
if (mediaInfo == null) return null;
if (!context.mounted) return null;
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
return compressedAttachment;
}
Future<void> uploadSingleAttachment(BuildContext context, int idx) async { Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
if (isBusy) return; if (isBusy) return;
@@ -297,17 +342,26 @@ class PostWriteController extends ChangeNotifier {
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
); );
final item = await attach.chunkedUploadParts( var item = await attach.chunkedUploadParts(
media.toFile()!, media.toFile()!,
place.$1, place.$1,
place.$2, place.$2,
onProgress: (progress) { onProgress: (value) {
// Calculate overall progress for attachments // Calculate overall progress for attachments
progress = math.max(((i + progress) / attachments.length) * kAttachmentProgressWeight, progress); progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
notifyListeners(); notifyListeners();
}, },
); );
try {
final compressedAttachment = await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) {
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
}
} catch (err) {
if (context.mounted) context.showErrorDialog(err);
}
progress = (i + 1) / attachments.length * kAttachmentProgressWeight; progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
attachments[i] = PostWriteMedia(item); attachments[i] = PostWriteMedia(item);
notifyListeners(); notifyListeners();

View File

@@ -21,7 +21,7 @@ class SnAttachmentProvider {
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) { void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
for (final item in items) { for (final item in items) {
if ((item.isAnalyzed && item.isUploaded) || noCheck) { if (item.isAnalyzed || noCheck) {
_cache[item.rid] = item; _cache[item.rid] = item;
} }
} }
@@ -34,7 +34,7 @@ class SnAttachmentProvider {
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta'); final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
final out = SnAttachment.fromJson(resp.data); final out = SnAttachment.fromJson(resp.data);
if (out.isAnalyzed && out.isUploaded) { if (out.isAnalyzed) {
_cache[rid] = out; _cache[rid] = out;
} }
@@ -62,11 +62,12 @@ class SnAttachmentProvider {
'id': pendingFetch.join(','), 'id': pendingFetch.join(','),
}, },
); );
final out = resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).toList(); final List<SnAttachment?> out =
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
for (final item in out) { for (final item in out) {
if (item == null) continue; if (item == null) continue;
if (item.isAnalyzed && item.isUploaded) { if (item.isAnalyzed) {
_cache[item.rid] = item; _cache[item.rid] = item;
} }
result[randomMapping[item.rid]!] = item; result[randomMapping[item.rid]!] = item;
@@ -85,6 +86,7 @@ class SnAttachmentProvider {
Map<String, dynamic>? metadata, { Map<String, dynamic>? metadata, {
String? mimetype, String? mimetype,
Function(double progress)? onProgress, Function(double progress)? onProgress,
bool analyzeNow = false,
}) async { }) async {
final filePayload = MultipartFile.fromBytes(data, filename: filename); final filePayload = MultipartFile.fromBytes(data, filename: filename);
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename; final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
@@ -107,6 +109,7 @@ class SnAttachmentProvider {
final resp = await _sn.client.post( final resp = await _sn.client.post(
'/cgi/uc/attachments', '/cgi/uc/attachments',
data: formData, data: formData,
queryParameters: {'analyzeNow': analyzeNow},
onSendProgress: (count, total) { onSendProgress: (count, total) {
if (onProgress != null) { if (onProgress != null) {
onProgress(count / total); onProgress(count / total);
@@ -117,7 +120,7 @@ class SnAttachmentProvider {
return SnAttachment.fromJson(resp.data); return SnAttachment.fromJson(resp.data);
} }
Future<(SnAttachment, int)> chunkedUploadInitialize( Future<(SnAttachmentFragment, int)> chunkedUploadInitialize(
int size, int size,
String filename, String filename,
String pool, String pool,
@@ -134,7 +137,7 @@ class SnAttachmentProvider {
mimetypeOverride = mimetype; mimetypeOverride = mimetype;
} }
final resp = await _sn.client.post('/cgi/uc/attachments/multipart', data: { final resp = await _sn.client.post('/cgi/uc/fragments', data: {
'alt': fileAlt, 'alt': fileAlt,
'name': filename, 'name': filename,
'pool': pool, 'pool': pool,
@@ -143,18 +146,20 @@ class SnAttachmentProvider {
if (mimetypeOverride != null) 'mimetype': mimetypeOverride, if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
}); });
return (SnAttachment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int); return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
} }
Future<SnAttachment> _chunkedUploadOnePart( Future<dynamic> _chunkedUploadOnePart(
Uint8List data, Uint8List data,
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/attachments/multipart/$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) {
@@ -163,21 +168,28 @@ class SnAttachmentProvider {
}, },
); );
return SnAttachment.fromJson(resp.data); if (resp.data['attachment'] != null) {
return SnAttachment.fromJson(resp.data['attachment']);
} else {
return SnAttachmentFragment.fromJson(resp.data['fragment']);
}
} }
Future<SnAttachment> chunkedUploadParts( Future<SnAttachment> chunkedUploadParts(
XFile file, XFile file,
SnAttachment 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 currentTask = 0; var completedTasks = 0;
final queue = Queue<Future<void>>(); final queue = Queue<Future<void>>();
final activeTasks = <Future<void>>[]; final activeTasks = <Future<void>>[];
late SnAttachment out;
for (final entry in chunks.entries) { for (final entry in chunks.entries) {
queue.add(() async { queue.add(() async {
final beginCursor = entry.value * chunkSize; final beginCursor = entry.value * chunkSize;
@@ -187,16 +199,26 @@ class SnAttachmentProvider {
); );
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList()); final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
place = await _chunkedUploadOnePart( final result = await _chunkedUploadOnePart(
data, data,
place.rid, place.rid,
entry.key, entry.key,
analyzeNow: analyzeNow,
onProgress: (progress) {
final overallProgress = (completedTasks + progress) / chunks.length;
onProgress?.call(overallProgress);
},
); );
final overallProgress = currentTask / chunks.length; completedTasks++;
final overallProgress = completedTasks / chunks.length;
onProgress?.call(overallProgress); onProgress?.call(overallProgress);
currentTask++; if (result is SnAttachmentFragment) {
place = result;
} else {
out = result as SnAttachment;
}
}()); }());
} }
@@ -213,21 +235,23 @@ class SnAttachmentProvider {
} }
} }
return place; return out;
} }
Future<SnAttachment> updateOne( Future<SnAttachment> updateOne(
int id, { SnAttachment item, {
String? alt, String? alt,
String? thumbnail, int? thumbnailId,
int? compressedId,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
bool? isIndexable, bool? isIndexable,
}) async { }) async {
final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: { final resp = await _sn.client.put('/cgi/uc/attachments/${item.id}', data: {
'alt': alt, 'alt': alt ?? item.alt,
'thumbnail': thumbnail, 'thumbnail': thumbnailId ?? item.thumbnailId,
'metadata': metadata, 'compressed': compressedId ?? item.compressedId,
'is_indexable': isIndexable, 'metadata': metadata ?? item.usermeta,
'is_indexable': isIndexable ?? item.isIndexable,
}); });
return SnAttachment.fromJson(resp.data); return SnAttachment.fromJson(resp.data);
} }

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

@@ -19,7 +19,7 @@ class SnAttachment with _$SnAttachment {
required int id, required int id,
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
required dynamic deletedAt, required DateTime? deletedAt,
required String rid, required String rid,
required String uuid, required String uuid,
required int size, required int size,
@@ -31,19 +31,22 @@ class SnAttachment with _$SnAttachment {
required int refCount, required int refCount,
@Default(0) int contentRating, @Default(0) int contentRating,
@Default(0) int qualityRating, @Default(0) int qualityRating,
required dynamic fileChunks, required DateTime? cleanedAt,
required dynamic cleanedAt,
required bool isAnalyzed, required bool isAnalyzed,
required bool isUploaded,
required bool isSelfRef, required bool isSelfRef,
required dynamic ref, required bool isIndexable,
required dynamic refId, required SnAttachment? ref,
required int? refId,
required SnAttachmentPool? pool, required SnAttachmentPool? pool,
required int poolId, required int? poolId,
required int accountId, required int accountId,
int? thumbnailId,
SnAttachment? thumbnail,
int? compressedId,
SnAttachment? compressed,
@Default([]) List<SnAttachmentBoost> boosts,
@Default({}) Map<String, dynamic> usermeta, @Default({}) Map<String, dynamic> usermeta,
@Default({}) Map<String, dynamic> metadata, @Default({}) Map<String, dynamic> metadata,
String? thumbnail,
}) = _SnAttachment; }) = _SnAttachment;
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json); factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json);
@@ -61,6 +64,37 @@ class SnAttachment with _$SnAttachment {
}; };
} }
@freezed
class SnAttachmentFragment with _$SnAttachmentFragment {
const SnAttachmentFragment._();
const factory SnAttachmentFragment({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String rid,
required String uuid,
required int size,
required String name,
required String alt,
required String mimetype,
required String hash,
String? fingerprint,
@Default({}) Map<String, int> fileChunks,
@Default([]) List<String> fileChunksMissing,
}) = _SnAttachmentFragment;
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) => _$SnAttachmentFragmentFromJson(json);
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
'image' => SnMediaType.image,
'video' => SnMediaType.video,
'audio' => SnMediaType.audio,
_ => SnMediaType.file,
};
}
@freezed @freezed
class SnAttachmentPool with _$SnAttachmentPool { class SnAttachmentPool with _$SnAttachmentPool {
const factory SnAttachmentPool({ const factory SnAttachmentPool({
@@ -77,3 +111,33 @@ class SnAttachmentPool with _$SnAttachmentPool {
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json); factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
} }
@freezed
class SnAttachmentDestination with _$SnAttachmentDestination {
const factory SnAttachmentDestination({
@Default(0) int id,
required String type,
required String label,
required String region,
required bool isBoost,
}) = _SnAttachmentDestination;
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json);
}
@freezed
class SnAttachmentBoost with _$SnAttachmentBoost {
const factory SnAttachmentBoost({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required int status,
required int destination,
required int attachmentId,
required SnAttachment attachment,
required int account,
}) = _SnAttachmentBoost;
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,9 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'], deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
rid: json['rid'] as String, rid: json['rid'] as String,
uuid: json['uuid'] as String, uuid: json['uuid'] as String,
size: (json['size'] as num).toInt(), size: (json['size'] as num).toInt(),
@@ -23,21 +25,36 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
refCount: (json['ref_count'] as num).toInt(), refCount: (json['ref_count'] as num).toInt(),
contentRating: (json['content_rating'] as num?)?.toInt() ?? 0, contentRating: (json['content_rating'] as num?)?.toInt() ?? 0,
qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0, qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0,
fileChunks: json['file_chunks'], cleanedAt: json['cleaned_at'] == null
cleanedAt: json['cleaned_at'], ? null
: DateTime.parse(json['cleaned_at'] as String),
isAnalyzed: json['is_analyzed'] as bool, isAnalyzed: json['is_analyzed'] as bool,
isUploaded: json['is_uploaded'] as bool,
isSelfRef: json['is_self_ref'] as bool, isSelfRef: json['is_self_ref'] as bool,
ref: json['ref'], isIndexable: json['is_indexable'] as bool,
refId: json['ref_id'], ref: json['ref'] == null
? null
: SnAttachment.fromJson(json['ref'] as Map<String, dynamic>),
refId: (json['ref_id'] as num?)?.toInt(),
pool: json['pool'] == null pool: json['pool'] == null
? null ? null
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>), : SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
poolId: (json['pool_id'] as num).toInt(), poolId: (json['pool_id'] as num?)?.toInt(),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
thumbnail: json['thumbnail'] == null
? null
: SnAttachment.fromJson(json['thumbnail'] as Map<String, dynamic>),
compressedId: (json['compressed_id'] as num?)?.toInt(),
compressed: json['compressed'] == null
? null
: SnAttachment.fromJson(json['compressed'] as Map<String, dynamic>),
boosts: (json['boosts'] as List<dynamic>?)
?.map(
(e) => SnAttachmentBoost.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {}, usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
metadata: json['metadata'] as Map<String, dynamic>? ?? const {}, metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
thumbnail: json['thumbnail'] as String?,
); );
Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
@@ -45,7 +62,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
'id': instance.id, 'id': instance.id,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt, 'deleted_at': instance.deletedAt?.toIso8601String(),
'rid': instance.rid, 'rid': instance.rid,
'uuid': instance.uuid, 'uuid': instance.uuid,
'size': instance.size, 'size': instance.size,
@@ -57,19 +74,68 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
'ref_count': instance.refCount, 'ref_count': instance.refCount,
'content_rating': instance.contentRating, 'content_rating': instance.contentRating,
'quality_rating': instance.qualityRating, 'quality_rating': instance.qualityRating,
'file_chunks': instance.fileChunks, 'cleaned_at': instance.cleanedAt?.toIso8601String(),
'cleaned_at': instance.cleanedAt,
'is_analyzed': instance.isAnalyzed, 'is_analyzed': instance.isAnalyzed,
'is_uploaded': instance.isUploaded,
'is_self_ref': instance.isSelfRef, 'is_self_ref': instance.isSelfRef,
'ref': instance.ref, 'is_indexable': instance.isIndexable,
'ref': instance.ref?.toJson(),
'ref_id': instance.refId, 'ref_id': instance.refId,
'pool': instance.pool?.toJson(), 'pool': instance.pool?.toJson(),
'pool_id': instance.poolId, 'pool_id': instance.poolId,
'account_id': instance.accountId, 'account_id': instance.accountId,
'thumbnail_id': instance.thumbnailId,
'thumbnail': instance.thumbnail?.toJson(),
'compressed_id': instance.compressedId,
'compressed': instance.compressed?.toJson(),
'boosts': instance.boosts.map((e) => e.toJson()).toList(),
'usermeta': instance.usermeta, 'usermeta': instance.usermeta,
'metadata': instance.metadata, 'metadata': instance.metadata,
'thumbnail': instance.thumbnail, };
_$SnAttachmentFragmentImpl _$$SnAttachmentFragmentImplFromJson(
Map<String, dynamic> json) =>
_$SnAttachmentFragmentImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
rid: json['rid'] as String,
uuid: json['uuid'] as String,
size: (json['size'] as num).toInt(),
name: json['name'] as String,
alt: json['alt'] as String,
mimetype: json['mimetype'] as String,
hash: json['hash'] as String,
fingerprint: json['fingerprint'] as String?,
fileChunks: (json['file_chunks'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
) ??
const {},
fileChunksMissing: (json['file_chunks_missing'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$SnAttachmentFragmentImplToJson(
_$SnAttachmentFragmentImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'rid': instance.rid,
'uuid': instance.uuid,
'size': instance.size,
'name': instance.name,
'alt': instance.alt,
'mimetype': instance.mimetype,
'hash': instance.hash,
'fingerprint': instance.fingerprint,
'file_chunks': instance.fileChunks,
'file_chunks_missing': instance.fileChunksMissing,
}; };
_$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson( _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson(
@@ -101,3 +167,54 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
'config': instance.config, 'config': instance.config,
'account_id': instance.accountId, 'account_id': instance.accountId,
}; };
_$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson(
Map<String, dynamic> json) =>
_$SnAttachmentDestinationImpl(
id: (json['id'] as num?)?.toInt() ?? 0,
type: json['type'] as String,
label: json['label'] as String,
region: json['region'] as String,
isBoost: json['is_boost'] as bool,
);
Map<String, dynamic> _$$SnAttachmentDestinationImplToJson(
_$SnAttachmentDestinationImpl instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'label': instance.label,
'region': instance.region,
'is_boost': instance.isBoost,
};
_$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson(
Map<String, dynamic> json) =>
_$SnAttachmentBoostImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
status: (json['status'] as num).toInt(),
destination: (json['destination'] as num).toInt(),
attachmentId: (json['attachment_id'] as num).toInt(),
attachment:
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
account: (json['account'] as num).toInt(),
);
Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
_$SnAttachmentBoostImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'status': instance.status,
'destination': instance.destination,
'attachment_id': instance.attachmentId,
'attachment': instance.attachment.toJson(),
'account': instance.account,
};

View File

@@ -10,8 +10,8 @@ import 'package:surface/widgets/dialog.dart';
class AttachmentInputDialog extends StatefulWidget { class AttachmentInputDialog extends StatefulWidget {
final String? title; final String? title;
final bool? analyzeNow;
const AttachmentInputDialog({super.key, required this.title}); const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
@override @override
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState(); State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
@@ -53,6 +53,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
_thumbnailFile!.path, _thumbnailFile!.path,
'interactive', 'interactive',
null, null,
analyzeNow: widget.analyzeNow ?? false,
); );
if (!mounted) return; if (!mounted) return;
Navigator.pop(context, attachment); Navigator.pop(context, attachment);
@@ -77,7 +78,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
controller: _randomIdController, controller: _randomIdController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'fieldAttachmentRandomId'.tr(), labelText: 'fieldAttachmentRandomId'.tr(),
border: const OutlineInputBorder(), border: const UnderlineInputBorder(),
isDense: true,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),

View File

@@ -1,7 +1,9 @@
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@@ -18,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,
}); });
@@ -41,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':
@@ -62,14 +66,12 @@ class AttachmentItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (data!.contentRating > 0) { if (data!.contentRating > 0) {
return LayoutBuilder( return LayoutBuilder(builder: (context, constraints) {
builder: (context, constraints) {
return _AttachmentItemSensitiveBlur( return _AttachmentItemSensitiveBlur(
isCompact: constraints.maxHeight < 360, isCompact: constraints.maxHeight < 360,
child: _buildContent(context), child: _buildContent(context),
); );
} });
);
} }
return _buildContent(context); return _buildContent(context);
@@ -176,6 +178,7 @@ class _AttachmentItemContentVideo extends StatefulWidget {
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> { class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
bool _showContent = false; bool _showContent = false;
bool _showOriginal = false;
Player? _videoPlayer; Player? _videoPlayer;
VideoController? _videoController; VideoController? _videoController;
@@ -184,15 +187,29 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
setState(() => _showContent = true); setState(() => _showContent = true);
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final url = sn.getAttachmentUrl(widget.data.rid); final url = _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid);
_videoPlayer = Player(); _videoPlayer = Player();
_videoController = VideoController(_videoPlayer!); _videoController = VideoController(_videoPlayer!);
_videoPlayer!.open(Media(url), play: !widget.isAutoload); _videoPlayer!.open(Media(url), play: !widget.isAutoload);
} }
void _toggleOriginal() {
if (!mounted) return;
if (widget.data.compressedId == null) return;
setState(() => _showOriginal = !_showOriginal);
final sn = context.read<SnNetworkProvider>();
_videoPlayer?.open(
Media(
_showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid),
),
play: true,
);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_showOriginal = widget.data.compressedId == null;
if (widget.isAutoload) _startLoad(); if (widget.isAutoload) _startLoad();
} }
@@ -215,9 +232,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: Stack( child: Stack(
children: [ children: [
if (widget.data.thumbnail?.isNotEmpty ?? false) if (widget.data.thumbnail != null)
AutoResizeUniversalImage( AutoResizeUniversalImage(
sn.getAttachmentUrl(widget.data.thumbnail!), sn.getAttachmentUrl(widget.data.thumbnail!.rid),
fit: BoxFit.cover, fit: BoxFit.cover,
) )
else else
@@ -297,9 +314,45 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
); );
} }
return Video( return MaterialDesktopVideoControlsTheme(
normal: MaterialDesktopVideoControlsThemeData(
buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white,
topButtonBarMargin: EdgeInsets.symmetric(horizontal: 12, vertical: 2),
topButtonBar: [
const Spacer(),
MaterialDesktopCustomButton(
iconSize: 24,
onPressed: _toggleOriginal,
icon: Builder(builder: (context) {
return _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24);
}),
),
],
),
fullscreen: const MaterialDesktopVideoControlsThemeData(),
child: MaterialVideoControlsTheme(
normal: MaterialVideoControlsThemeData(
buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white,
topButtonBarMargin: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
topButtonBar: [
const Spacer(),
MaterialDesktopCustomButton(
iconSize: 24,
onPressed: _toggleOriginal,
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24),
),
],
),
fullscreen: const MaterialVideoControlsThemeData(),
child: Video(
controller: _videoController!, controller: _videoController!,
aspectRatio: ratio, aspectRatio: ratio,
controls:
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls,
),
),
); );
} }

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,8 +14,9 @@ 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 EdgeInsets? listPadding;
@@ -23,8 +24,9 @@ class AttachmentList extends StatefulWidget {
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.listPadding,
}); });
@@ -53,7 +55,6 @@ class _AttachmentListState extends State<AttachmentList> {
final constraints = BoxConstraints( final constraints = BoxConstraints(
minWidth: 80, minWidth: 80,
maxHeight: widget.maxHeight ?? double.infinity, maxHeight: widget.maxHeight ?? double.infinity,
maxWidth: layoutConstraints.maxWidth - 20,
); );
if (widget.data.isEmpty) return const SizedBox.shrink(); if (widget.data.isEmpty) return const SizedBox.shrink();
@@ -66,28 +67,18 @@ class _AttachmentListState extends State<AttachmentList> {
} }
.toDouble(); .toDouble();
return Container( return Padding(
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE) padding: widget.listPadding ?? EdgeInsets.zero,
? constraints.copyWith( child: Container(
maxWidth: math.min( constraints: constraints,
constraints.maxWidth, width: double.infinity,
kAttachmentMaxWidth, child: GestureDetector(
),
)
: null,
child: AspectRatio( child: AspectRatio(
aspectRatio: singleAspectRatio, 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( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
border: Border(top: borderSide, bottom: borderSide), border: Border.fromBorderSide(borderSide),
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
), ),
child: ClipRRect( child: ClipRRect(
@@ -95,23 +86,10 @@ class _AttachmentListState extends State<AttachmentList> {
child: AttachmentItem( child: AttachmentItem(
data: widget.data[0], data: widget.data[0],
heroTag: heroTags[0], heroTag: heroTags[0],
fit: widget.fit,
), ),
), ),
), ),
);
}
return Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(top: borderSide, bottom: borderSide),
),
child: AttachmentItem(
data: widget.data[0],
heroTag: heroTags.first,
),
);
},
), ),
onTap: () { onTap: () {
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return; if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
@@ -130,14 +108,9 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
if (widget.isFlatted) { if (widget.gridded) {
return Wrap( return Padding(
spacing: 4, padding: widget.listPadding ?? EdgeInsets.zero,
runSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => AspectRatio(
aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
@@ -149,15 +122,39 @@ class _AttachmentListState extends State<AttachmentList> {
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius, 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( child: AttachmentItem(
data: ele, data: ele,
heroTag: heroTags[idx], heroTag: heroTags[idx],
fit: widget.fit,
), ),
), ),
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(), .toList(),
),
),
),
); );
} }

View File

@@ -0,0 +1,120 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/dialog.dart';
class PendingAttachmentBoostDialog extends StatefulWidget {
final PostWriteMedia media;
const PendingAttachmentBoostDialog({super.key, required this.media});
@override
State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState();
}
class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> {
List<SnAttachmentDestination>? _regions;
SnAttachmentDestination? _selectedRegion;
Future<void> _fetchRegions() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/uc/destinations');
setState(() {
_regions = List<SnAttachmentDestination>.from(
resp.data?.map((e) => SnAttachmentDestination.fromJson(e)) ?? [],
).cast<SnAttachmentDestination>().where((ele) => ele.isBoost).toList();
});
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
bool _isBusy = false;
Future<void> _performAction() async {
if (_isBusy) return;
if (_selectedRegion == null) return;
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.post('/cgi/uc/boosts', data: {
'attachment': widget.media.attachment!.id,
'destination': _selectedRegion!.id,
});
if (!mounted) return;
Navigator.pop(context, SnAttachmentBoost.fromJson(resp.data));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchRegions();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('attachmentBoost').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachmentBoostHint').tr(),
const Gap(16),
Text('attachmentDestinationRegion').tr().fontSize(18),
const Gap(8),
Card(
child: _regions == null
? const CircularProgressIndicator().center().padding(all: 16)
: Column(
children: _regions!.map(
(ele) {
return RadioListTile(
title: Text(ele.label).tr(),
subtitle: Text(
'attachmentDestinationRegion${ele.region}'.trExists()
? 'attachmentDestinationRegion${ele.region}'.tr()
: ele.region,
),
selected: _selectedRegion == ele,
value: ele,
groupValue: _selectedRegion,
onChanged: (value) {
if (value != null) setState(() => _selectedRegion = value);
},
);
},
).toList(),
),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () {
Navigator.pop(context);
},
child: Text('dialogDismiss'.tr()),
),
TextButton(
onPressed: _isBusy ? null : () => _performAction(),
child: Text('dialogConfirm'.tr()),
),
],
);
}
}

View File

@@ -159,6 +159,7 @@ class ChatMessage extends StatelessWidget {
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
gridded: true,
noGrow: true, noGrow: true,
maxHeight: 520, maxHeight: 520,
listPadding: const EdgeInsets.only(top: 8), listPadding: const EdgeInsets.only(top: 8),

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(() {

View File

@@ -18,7 +18,6 @@ import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/link_preview.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
@@ -85,7 +84,6 @@ class PostItem extends StatelessWidget {
child: MultiProvider( child: MultiProvider(
providers: [ providers: [
Provider<SnNetworkProvider>(create: (_) => context.read()), Provider<SnNetworkProvider>(create: (_) => context.read()),
Provider<SnLinkPreviewProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()), ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
], ],
child: ResponsiveBreakpoints.builder( child: ResponsiveBreakpoints.builder(
@@ -253,7 +251,9 @@ class PostItem extends StatelessWidget {
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
maxHeight: 560, gridded: true,
maxHeight: showFullPost ? null : 480,
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
listPadding: const EdgeInsets.symmetric(horizontal: 12), listPadding: const EdgeInsets.symmetric(horizontal: 12),
), ),
if (data.body['content'] != null) if (data.body['content'] != null)
@@ -334,17 +334,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( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
isFlatted: true, gridded: true,
).padding(horizontal: 16, bottom: 8), ).padding(horizontal: 16, bottom: 8),
if (data.body['content'] != null)
LinkPreviewWidget(
text: data.body['content'],
).padding(horizontal: 4),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -890,11 +885,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,
}); });
@@ -936,12 +929,14 @@ class _PostQuoteContent extends StatelessWidget {
), ),
child: AttachmentList( child: AttachmentList(
data: child.preload!.attachments!, data: child.preload!.attachments!,
isFlatted: isFlatted, maxHeight: 360,
fit: BoxFit.contain,
gridded: true,
listPadding: const EdgeInsets.symmetric(horizontal: 12), listPadding: 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

@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
@@ -20,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_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';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@@ -92,15 +94,23 @@ class PostMediaPendingList extends StatelessWidget {
context: context, context: context,
builder: (context) => AttachmentInputDialog( builder: (context) => AttachmentInputDialog(
title: 'attachmentSetThumbnail'.tr(), title: 'attachmentSetThumbnail'.tr(),
analyzeNow: true,
), ),
); );
if (thumbnail == null) return; if (thumbnail == null) return;
if (!context.mounted) return; if (!context.mounted) return;
try {
final attach = context.read<SnAttachmentProvider>(); final attach = context.read<SnAttachmentProvider>();
final newAttach = await attach.updateOne(attachments[idx].attachment!.id, thumbnail: thumbnail.rid); final newAttach = await attach.updateOne(
attachments[idx].attachment!,
thumbnailId: thumbnail.id,
);
onUpdate!(idx, PostWriteMedia(newAttach)); onUpdate!(idx, PostWriteMedia(newAttach));
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
} }
Future<void> _deleteAttachment(BuildContext context, int idx) async { Future<void> _deleteAttachment(BuildContext context, int idx) async {
@@ -120,6 +130,23 @@ class PostMediaPendingList extends StatelessWidget {
} }
} }
Future<void> _createBoost(BuildContext context, int idx) async {
if (attachments[idx].attachment == null) return;
final result = await showDialog<SnAttachmentBoost?>(
context: context,
builder: (context) => PendingAttachmentBoostDialog(media: attachments[idx]),
);
if (result == null) return;
final newAttach = attachments[idx].attachment!.copyWith(
boosts: [...attachments[idx].attachment!.boosts, result],
);
final newMedia = PostWriteMedia(newAttach);
onUpdate!(idx, newMedia);
}
Future<void> _compressVideo(BuildContext context, int idx) async { Future<void> _compressVideo(BuildContext context, int idx) async {
final result = await showDialog<PostWriteMedia?>( final result = await showDialog<PostWriteMedia?>(
context: context, context: context,
@@ -142,6 +169,14 @@ class PostMediaPendingList extends StatelessWidget {
_compressVideo(context, idx); _compressVideo(context, idx);
}, },
), ),
if (media.attachment != null)
MenuItem(
label: 'attachmentBoost'.tr(),
icon: Symbols.bolt,
onSelected: () {
_createBoost(context, idx);
},
),
if (media.attachment != null && media.type == SnMediaType.video) if (media.attachment != null && media.type == SnMediaType.video)
MenuItem( MenuItem(
label: 'attachmentSetThumbnail'.tr(), label: 'attachmentSetThumbnail'.tr(),
@@ -290,15 +325,16 @@ class _PostMediaPendingItem extends StatelessWidget {
width: 1, width: 1,
), ),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surfaceContainer,
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio( child: Row(
children: [
AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: switch (media.type) { child: switch (media.type) {
SnMediaType.image => Container( SnMediaType.image => LayoutBuilder(builder: (context, constraints) {
color: Theme.of(context).colorScheme.surfaceContainer,
child: LayoutBuilder(builder: (context, constraints) {
return Image( return Image(
image: media.getImageProvider( image: media.getImageProvider(
context, context,
@@ -308,14 +344,11 @@ class _PostMediaPendingItem extends StatelessWidget {
fit: BoxFit.contain, fit: BoxFit.contain,
); );
}), }),
), SnMediaType.video => Stack(
SnMediaType.video => Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (media.attachment?.thumbnail != null) if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!)), AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
const Icon(Symbols.videocam, color: Colors.white, shadows: [ const Icon(Symbols.videocam, color: Colors.white, shadows: [
Shadow( Shadow(
offset: Offset(1, 1), offset: Offset(1, 1),
@@ -325,14 +358,11 @@ class _PostMediaPendingItem extends StatelessWidget {
]), ]),
], ],
), ),
), SnMediaType.audio => Stack(
SnMediaType.audio => Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (media.attachment?.thumbnail != null) if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!)), AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
const Icon(Symbols.audio_file, color: Colors.white, shadows: [ const Icon(Symbols.audio_file, color: Colors.white, shadows: [
Shadow( Shadow(
offset: Offset(1, 1), offset: Offset(1, 1),
@@ -342,13 +372,91 @@ class _PostMediaPendingItem extends StatelessWidget {
]), ]),
], ],
), ),
),
_ => Container( _ => Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: const Icon(Symbols.docs).center(), child: const Icon(Symbols.docs).center(),
), ),
}, },
), ),
if (media.type != SnMediaType.image) const VerticalDivider(width: 1, thickness: 1),
if (media.type != SnMediaType.image)
SizedBox(
width: 160,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (media.attachment != null)
Text(
media.attachment!.alt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
else if (media.file != null)
Text(media.file!.name, maxLines: 1, overflow: TextOverflow.ellipsis)
else
Text('unknown'.tr()),
if (media.attachment != null)
Text(
media.attachment!.size.formatBytes(),
style: GoogleFonts.robotoMono(fontSize: 13),
maxLines: 1,
)
else if (media.file != null)
FutureBuilder<int?>(
future: media.length(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink();
return Text(
snapshot.data!.formatBytes(),
style: GoogleFonts.robotoMono(fontSize: 13),
maxLines: 1,
);
},
),
],
),
),
if (media.attachment != null && media.attachment!.boosts.isNotEmpty)
Row(
children: [
Icon(Symbols.bolt, size: 16),
const Gap(4),
Text('attachmentGotBoosted').tr().fontSize(13),
],
),
if (media.attachment != null && media.attachment!.compressedId != null)
Row(
children: [
Icon(Symbols.compress, size: 16),
const Gap(4),
Text('attachmentCopyCompressed').tr().fontSize(13),
],
),
if (media.attachment != null)
Row(
children: [
Icon(Symbols.cloud, size: 16),
const Gap(4),
Text('attachmentUploaded').tr().fontSize(13),
],
)
else
Row(
children: [
Icon(Symbols.cloud_off, size: 16),
const Gap(4),
Text('attachmentPending').tr().fontSize(13),
],
),
],
),
).padding(horizontal: 12, vertical: 12),
],
),
), ),
); );
} }

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.2): - livekit_client (2.3.4):
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@@ -170,6 +170,8 @@ PODS:
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1): - url_launcher_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- video_compress (0.3.0):
- FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (125.6422.06) - WebRTC-SDK (125.6422.06)
@@ -201,6 +203,7 @@ DEPENDENCIES:
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - 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`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
SPEC REPOS: SPEC REPOS:
@@ -272,6 +275,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos: url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_compress:
:path: Flutter/ephemeral/.symlinks/plugins/video_compress/macos
wakelock_plus: wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
@@ -299,7 +304,7 @@ SPEC CHECKSUMS:
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: 9fdcb22df3de55e6d4b24bdc3b5eb1c0269d774a 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
@@ -314,6 +319,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db

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.1.1+39 version: 2.2.1+41
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4