Compare commits
14 Commits
2c7dc8c2ea
...
2.2.1+42
Author | SHA1 | Date | |
---|---|---|---|
|
96fd64d85d | ||
|
e236b7f98b | ||
|
5c7929e618 | ||
|
7ba5260246 | ||
|
a6d4947a23 | ||
|
7fbd4e9647 | ||
|
95d926b29f | ||
|
f6cf6d0440 | ||
|
e503c3f02f | ||
|
d4fbdd397e | ||
|
03943a7138 | ||
|
44f2c5fe0e | ||
|
bb66d5b684 | ||
|
1fca36293d |
30
api/Paperclip/Activate Boost.bru
Normal file
30
api/Paperclip/Activate Boost.bru
Normal 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
|
||||
}
|
||||
}
|
@@ -298,6 +298,26 @@
|
||||
"attachmentInputDialog": "Upload attachments",
|
||||
"attachmentInputUseRandomId": "Use Random ID",
|
||||
"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",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "All notifications read",
|
||||
@@ -438,6 +458,7 @@
|
||||
"accountJoinedAt": "Joined at {}",
|
||||
"accountBirthday": "Born on {}",
|
||||
"accountBadge": "Badge",
|
||||
"accountCheckInNoRecords": "No check-in records",
|
||||
"badgeCompanyStaff": "Solsynth Staff",
|
||||
"badgeSiteMigration": "Solar Network Native",
|
||||
"accountStatus": "Status",
|
||||
@@ -446,6 +467,7 @@
|
||||
"accountStatusLastSeen": "Last seen at {}",
|
||||
"postArticle": "Article on the Solar Network",
|
||||
"postStory": "Story on the Solar Network",
|
||||
"postLocalDraftRestored": "Restored from device",
|
||||
"articleWrittenAt": "Written at {}",
|
||||
"articleEditedAt": "Edited at {}",
|
||||
"attachmentSaved": "Saved to album",
|
||||
@@ -489,7 +511,7 @@
|
||||
"appInitializing": "Initializing",
|
||||
"poweredBy": "Powered by {}",
|
||||
"shareIntent": "Share",
|
||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||
"shareIntentPostStory": "Post a Story",
|
||||
"updateAvailable": "Update Available",
|
||||
"updateOngoing": "Updating, please wait...",
|
||||
@@ -513,12 +535,5 @@
|
||||
"postCategoryKnowledge": "Knowledge",
|
||||
"postCategoryLiterature": "Literature",
|
||||
"postCategoryFunny": "Funny",
|
||||
"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."
|
||||
"postCategoryUncategorized": "Uncategorized"
|
||||
}
|
||||
|
@@ -296,6 +296,26 @@
|
||||
"attachmentInputDialog": "上传附件",
|
||||
"attachmentInputUseRandomId": "使用访问 ID",
|
||||
"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": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "无未读通知",
|
||||
@@ -436,6 +456,7 @@
|
||||
"accountJoinedAt": "加入于 {}",
|
||||
"accountBirthday": "出生于 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暂无运势记录",
|
||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "状态",
|
||||
@@ -444,6 +465,7 @@
|
||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "从本地恢复草稿",
|
||||
"articleWrittenAt": "发表于 {}",
|
||||
"articleEditedAt": "编辑于 {}",
|
||||
"attachmentSaved": "已保存到相册",
|
||||
@@ -511,12 +533,5 @@
|
||||
"postCategoryKnowledge": "知识",
|
||||
"postCategoryLiterature": "文学",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分类",
|
||||
"waitingForUpload": "等待上传",
|
||||
"attachmentCompressQuality": "压缩质量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默认",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。"
|
||||
"postCategoryUncategorized": "未分类"
|
||||
}
|
||||
|
@@ -296,6 +296,26 @@
|
||||
"attachmentInputDialog": "上傳附件",
|
||||
"attachmentInputUseRandomId": "使用訪問 ID",
|
||||
"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": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "無未讀通知",
|
||||
@@ -436,6 +456,7 @@
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
@@ -444,6 +465,7 @@
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
@@ -511,12 +533,5 @@
|
||||
"postCategoryKnowledge": "知識",
|
||||
"postCategoryLiterature": "文學",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分類",
|
||||
"waitingForUpload": "等待上傳",
|
||||
"attachmentCompressQuality": "壓縮質量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默認",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
|
||||
"postCategoryUncategorized": "未分類"
|
||||
}
|
||||
|
@@ -296,6 +296,26 @@
|
||||
"attachmentInputDialog": "上傳附件",
|
||||
"attachmentInputUseRandomId": "使用訪問 ID",
|
||||
"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": "通知",
|
||||
"notificationUnreadCount": {
|
||||
"zero": "無未讀通知",
|
||||
@@ -436,6 +456,7 @@
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
@@ -444,6 +465,7 @@
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
@@ -511,12 +533,5 @@
|
||||
"postCategoryKnowledge": "知識",
|
||||
"postCategoryLiterature": "文學",
|
||||
"postCategoryFunny": "搞笑",
|
||||
"postCategoryUncategorized": "未分類",
|
||||
"waitingForUpload": "等待上傳",
|
||||
"attachmentCompressQuality": "壓縮質量",
|
||||
"attachmentCompressQualityHighest": "最高",
|
||||
"attachmentCompressQualityDefault": "默認",
|
||||
"attachmentCompressQualityMedium": "中等",
|
||||
"attachmentCompressQualityLow": "低",
|
||||
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
|
||||
"postCategoryUncategorized": "未分類"
|
||||
}
|
||||
|
@@ -173,7 +173,7 @@ PODS:
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.1.3)
|
||||
- livekit_client (2.3.3):
|
||||
- livekit_client (2.3.4):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@@ -211,9 +211,6 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
@@ -259,7 +256,6 @@ DEPENDENCIES:
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_compress (from `.symlinks/plugins/video_compress/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
@@ -347,8 +343,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_compress:
|
||||
@@ -391,7 +385,7 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||
livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef
|
||||
livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
@@ -407,7 +401,6 @@ SPEC CHECKSUMS:
|
||||
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||
|
@@ -1,12 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
@@ -14,6 +19,7 @@ import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
class PostWriteMedia {
|
||||
late String name;
|
||||
@@ -149,8 +155,18 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
|
||||
PostWriteController() {
|
||||
titleController.addListener(() => notifyListeners());
|
||||
descriptionController.addListener(() => notifyListeners());
|
||||
titleController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
descriptionController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
contentController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
});
|
||||
_temporaryLoad();
|
||||
}
|
||||
|
||||
String mode = kTitleMap.keys.first;
|
||||
@@ -229,7 +245,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 place = await attach.chunkedUploadInitialize(
|
||||
@@ -240,19 +257,136 @@ class PostWriteController extends ChangeNotifier {
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
);
|
||||
|
||||
final item = await attach.chunkedUploadParts(
|
||||
var item = await attach.chunkedUploadParts(
|
||||
media.toFile()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
progress = progress;
|
||||
analyzeNow: media.type == SnMediaType.image,
|
||||
onProgress: (value) {
|
||||
progress = value;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static const kTemporaryStorageKey = 'int_draft_post';
|
||||
|
||||
Timer? _temporarySaveTimer;
|
||||
|
||||
void _temporaryPlanSave() {
|
||||
_temporarySaveTimer?.cancel();
|
||||
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
|
||||
_temporarySave();
|
||||
log("[PostWriter] Temporary save saved.");
|
||||
});
|
||||
}
|
||||
|
||||
void _temporarySave() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
if (titleController.text.isEmpty &&
|
||||
descriptionController.text.isEmpty &&
|
||||
contentController.text.isEmpty &&
|
||||
thumbnail == null &&
|
||||
attachments.isEmpty) {
|
||||
prefs.remove(kTemporaryStorageKey);
|
||||
return;
|
||||
}
|
||||
|
||||
prefs.setString(
|
||||
kTemporaryStorageKey,
|
||||
jsonEncode({
|
||||
'publisher': publisher,
|
||||
'content': contentController.text,
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
bool temporaryRestored = false;
|
||||
|
||||
void _temporaryLoad() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
final raw = prefs.getString(kTemporaryStorageKey);
|
||||
if (raw == null) return;
|
||||
final data = jsonDecode(raw);
|
||||
contentController.text = data['content'];
|
||||
aliasController.text = data['alias'] ?? '';
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments
|
||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||
visibility = data['visibility'];
|
||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
temporaryRestored = true;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||
if (isBusy) return;
|
||||
|
||||
@@ -297,17 +431,28 @@ class PostWriteController extends ChangeNotifier {
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
);
|
||||
|
||||
final item = await attach.chunkedUploadParts(
|
||||
var item = await attach.chunkedUploadParts(
|
||||
media.toFile()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
onProgress: (value) {
|
||||
// Calculate overall progress for attachments
|
||||
progress = math.max(((i + progress) / attachments.length) * kAttachmentProgressWeight, progress);
|
||||
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
if (context.mounted) {
|
||||
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;
|
||||
attachments[i] = PostWriteMedia(item);
|
||||
notifyListeners();
|
||||
@@ -361,6 +506,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
reset();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -409,56 +555,67 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
void setPublisher(SnPublisher? item) {
|
||||
publisher = item;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedAt(DateTime? value) {
|
||||
publishedAt = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedUntil(DateTime? value) {
|
||||
publishedUntil = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setTags(List<String> value) {
|
||||
tags = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCategories(List<String> value) {
|
||||
categories = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibility(int value) {
|
||||
visibility = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibleUsers(List<int> value) {
|
||||
visibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setInvisibleUsers(List<int> value) {
|
||||
invisibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setProgress(double? value) {
|
||||
progress = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setIsBusy(bool value) {
|
||||
isBusy = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMode(String value) {
|
||||
mode = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -476,6 +633,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
replyingPost = null;
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
temporaryRestored = false;
|
||||
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@@ -86,6 +86,7 @@ class SnAttachmentProvider {
|
||||
Map<String, dynamic>? metadata, {
|
||||
String? mimetype,
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||
@@ -108,6 +109,7 @@ class SnAttachmentProvider {
|
||||
final resp = await _sn.client.post(
|
||||
'/cgi/uc/attachments',
|
||||
data: formData,
|
||||
queryParameters: {'analyzeNow': analyzeNow},
|
||||
onSendProgress: (count, total) {
|
||||
if (onProgress != null) {
|
||||
onProgress(count / total);
|
||||
@@ -152,10 +154,12 @@ class SnAttachmentProvider {
|
||||
String rid,
|
||||
String cid, {
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final resp = await _sn.client.post(
|
||||
'/cgi/uc/fragments/$rid/$cid',
|
||||
data: data,
|
||||
queryParameters: {'analyzeNow': analyzeNow},
|
||||
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
||||
onSendProgress: (count, total) {
|
||||
if (onProgress != null) {
|
||||
@@ -176,9 +180,10 @@ class SnAttachmentProvider {
|
||||
SnAttachmentFragment place,
|
||||
int chunkSize, {
|
||||
Function(double progress)? onProgress,
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final Map<String, dynamic> chunks = place.fileChunks;
|
||||
var currentTask = 0;
|
||||
var completedTasks = 0;
|
||||
|
||||
final queue = Queue<Future<void>>();
|
||||
final activeTasks = <Future<void>>[];
|
||||
@@ -198,14 +203,15 @@ class SnAttachmentProvider {
|
||||
data,
|
||||
place.rid,
|
||||
entry.key,
|
||||
analyzeNow: analyzeNow,
|
||||
onProgress: (progress) {
|
||||
final overallProgress = (currentTask + progress) / chunks.length;
|
||||
final overallProgress = (completedTasks + progress) / chunks.length;
|
||||
onProgress?.call(overallProgress);
|
||||
},
|
||||
);
|
||||
|
||||
currentTask++;
|
||||
final overallProgress = currentTask / chunks.length;
|
||||
completedTasks++;
|
||||
final overallProgress = completedTasks / chunks.length;
|
||||
onProgress?.call(overallProgress);
|
||||
|
||||
if (result is SnAttachmentFragment) {
|
||||
@@ -233,19 +239,19 @@ class SnAttachmentProvider {
|
||||
}
|
||||
|
||||
Future<SnAttachment> updateOne(
|
||||
int id, {
|
||||
SnAttachment item, {
|
||||
String? alt,
|
||||
int? thumbnailId,
|
||||
int? compressedId,
|
||||
Map<String, dynamic>? metadata,
|
||||
bool? isIndexable,
|
||||
}) async {
|
||||
final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: {
|
||||
'alt': alt,
|
||||
'thumbnail': thumbnailId,
|
||||
'compressed': compressedId,
|
||||
'metadata': metadata,
|
||||
'is_indexable': isIndexable,
|
||||
final resp = await _sn.client.put('/cgi/uc/attachments/${item.id}', data: {
|
||||
'alt': alt ?? item.alt,
|
||||
'thumbnail': thumbnailId ?? item.thumbnailId,
|
||||
'compressed': compressedId ?? item.compressedId,
|
||||
'metadata': metadata ?? item.usermeta,
|
||||
'is_indexable': isIndexable ?? item.isIndexable,
|
||||
});
|
||||
return SnAttachment.fromJson(resp.data);
|
||||
}
|
||||
|
@@ -517,6 +517,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
future: _getCheckInRecords(),
|
||||
builder: (context, snapshot) {
|
||||
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!;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
|
@@ -364,6 +364,35 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _writeController.temporaryRestored
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.restore, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(child: Text('postLocalDraftRestored').tr()),
|
||||
InkWell(
|
||||
child: Text('dialogDismiss').tr(),
|
||||
onTap: () {
|
||||
_writeController.reset();
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
|
@@ -34,15 +34,17 @@ class SnAttachment with _$SnAttachment {
|
||||
required DateTime? cleanedAt,
|
||||
required bool isAnalyzed,
|
||||
required bool isSelfRef,
|
||||
required bool isIndexable,
|
||||
required SnAttachment? ref,
|
||||
required int? refId,
|
||||
required SnAttachmentPool? pool,
|
||||
required int poolId,
|
||||
required int? poolId,
|
||||
required int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
int? compressedId,
|
||||
SnAttachment? compressed,
|
||||
@Default([]) List<SnAttachmentBoost> boosts,
|
||||
@Default({}) Map<String, dynamic> usermeta,
|
||||
@Default({}) Map<String, dynamic> metadata,
|
||||
}) = _SnAttachment;
|
||||
@@ -109,3 +111,33 @@ class SnAttachmentPool with _$SnAttachmentPool {
|
||||
|
||||
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);
|
||||
}
|
||||
|
@@ -38,15 +38,17 @@ mixin _$SnAttachment {
|
||||
DateTime? get cleanedAt => throw _privateConstructorUsedError;
|
||||
bool get isAnalyzed => throw _privateConstructorUsedError;
|
||||
bool get isSelfRef => throw _privateConstructorUsedError;
|
||||
bool get isIndexable => throw _privateConstructorUsedError;
|
||||
SnAttachment? get ref => throw _privateConstructorUsedError;
|
||||
int? get refId => throw _privateConstructorUsedError;
|
||||
SnAttachmentPool? get pool => throw _privateConstructorUsedError;
|
||||
int get poolId => throw _privateConstructorUsedError;
|
||||
int? get poolId => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
int? get thumbnailId => throw _privateConstructorUsedError;
|
||||
SnAttachment? get thumbnail => throw _privateConstructorUsedError;
|
||||
int? get compressedId => throw _privateConstructorUsedError;
|
||||
SnAttachment? get compressed => throw _privateConstructorUsedError;
|
||||
List<SnAttachmentBoost> get boosts => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get usermeta => throw _privateConstructorUsedError;
|
||||
Map<String, dynamic> get metadata => throw _privateConstructorUsedError;
|
||||
|
||||
@@ -85,15 +87,17 @@ abstract class $SnAttachmentCopyWith<$Res> {
|
||||
DateTime? cleanedAt,
|
||||
bool isAnalyzed,
|
||||
bool isSelfRef,
|
||||
bool isIndexable,
|
||||
SnAttachment? ref,
|
||||
int? refId,
|
||||
SnAttachmentPool? pool,
|
||||
int poolId,
|
||||
int? poolId,
|
||||
int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
int? compressedId,
|
||||
SnAttachment? compressed,
|
||||
List<SnAttachmentBoost> boosts,
|
||||
Map<String, dynamic> usermeta,
|
||||
Map<String, dynamic> metadata});
|
||||
|
||||
@@ -136,15 +140,17 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
|
||||
Object? cleanedAt = freezed,
|
||||
Object? isAnalyzed = null,
|
||||
Object? isSelfRef = null,
|
||||
Object? isIndexable = null,
|
||||
Object? ref = freezed,
|
||||
Object? refId = freezed,
|
||||
Object? pool = freezed,
|
||||
Object? poolId = null,
|
||||
Object? poolId = freezed,
|
||||
Object? accountId = null,
|
||||
Object? thumbnailId = freezed,
|
||||
Object? thumbnail = freezed,
|
||||
Object? compressedId = freezed,
|
||||
Object? compressed = freezed,
|
||||
Object? boosts = null,
|
||||
Object? usermeta = null,
|
||||
Object? metadata = null,
|
||||
}) {
|
||||
@@ -221,6 +227,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
|
||||
? _value.isSelfRef
|
||||
: isSelfRef // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isIndexable: null == isIndexable
|
||||
? _value.isIndexable
|
||||
: isIndexable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ref: freezed == ref
|
||||
? _value.ref
|
||||
: ref // ignore: cast_nullable_to_non_nullable
|
||||
@@ -233,10 +243,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
|
||||
? _value.pool
|
||||
: pool // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachmentPool?,
|
||||
poolId: null == poolId
|
||||
poolId: freezed == poolId
|
||||
? _value.poolId
|
||||
: poolId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
as int?,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
@@ -257,6 +267,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
|
||||
? _value.compressed
|
||||
: compressed // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
boosts: null == boosts
|
||||
? _value.boosts
|
||||
: boosts // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachmentBoost>,
|
||||
usermeta: null == usermeta
|
||||
? _value.usermeta
|
||||
: usermeta // ignore: cast_nullable_to_non_nullable
|
||||
@@ -352,15 +366,17 @@ abstract class _$$SnAttachmentImplCopyWith<$Res>
|
||||
DateTime? cleanedAt,
|
||||
bool isAnalyzed,
|
||||
bool isSelfRef,
|
||||
bool isIndexable,
|
||||
SnAttachment? ref,
|
||||
int? refId,
|
||||
SnAttachmentPool? pool,
|
||||
int poolId,
|
||||
int? poolId,
|
||||
int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
int? compressedId,
|
||||
SnAttachment? compressed,
|
||||
List<SnAttachmentBoost> boosts,
|
||||
Map<String, dynamic> usermeta,
|
||||
Map<String, dynamic> metadata});
|
||||
|
||||
@@ -405,15 +421,17 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
|
||||
Object? cleanedAt = freezed,
|
||||
Object? isAnalyzed = null,
|
||||
Object? isSelfRef = null,
|
||||
Object? isIndexable = null,
|
||||
Object? ref = freezed,
|
||||
Object? refId = freezed,
|
||||
Object? pool = freezed,
|
||||
Object? poolId = null,
|
||||
Object? poolId = freezed,
|
||||
Object? accountId = null,
|
||||
Object? thumbnailId = freezed,
|
||||
Object? thumbnail = freezed,
|
||||
Object? compressedId = freezed,
|
||||
Object? compressed = freezed,
|
||||
Object? boosts = null,
|
||||
Object? usermeta = null,
|
||||
Object? metadata = null,
|
||||
}) {
|
||||
@@ -490,6 +508,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
|
||||
? _value.isSelfRef
|
||||
: isSelfRef // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isIndexable: null == isIndexable
|
||||
? _value.isIndexable
|
||||
: isIndexable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
ref: freezed == ref
|
||||
? _value.ref
|
||||
: ref // ignore: cast_nullable_to_non_nullable
|
||||
@@ -502,10 +524,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
|
||||
? _value.pool
|
||||
: pool // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachmentPool?,
|
||||
poolId: null == poolId
|
||||
poolId: freezed == poolId
|
||||
? _value.poolId
|
||||
: poolId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
as int?,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
@@ -526,6 +548,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
|
||||
? _value.compressed
|
||||
: compressed // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
boosts: null == boosts
|
||||
? _value._boosts
|
||||
: boosts // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachmentBoost>,
|
||||
usermeta: null == usermeta
|
||||
? _value._usermeta
|
||||
: usermeta // ignore: cast_nullable_to_non_nullable
|
||||
@@ -560,6 +586,7 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
required this.cleanedAt,
|
||||
required this.isAnalyzed,
|
||||
required this.isSelfRef,
|
||||
required this.isIndexable,
|
||||
required this.ref,
|
||||
required this.refId,
|
||||
required this.pool,
|
||||
@@ -569,9 +596,11 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
this.thumbnail,
|
||||
this.compressedId,
|
||||
this.compressed,
|
||||
final List<SnAttachmentBoost> boosts = const [],
|
||||
final Map<String, dynamic> usermeta = const {},
|
||||
final Map<String, dynamic> metadata = const {}})
|
||||
: _usermeta = usermeta,
|
||||
: _boosts = boosts,
|
||||
_usermeta = usermeta,
|
||||
_metadata = metadata,
|
||||
super._();
|
||||
|
||||
@@ -617,13 +646,15 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
@override
|
||||
final bool isSelfRef;
|
||||
@override
|
||||
final bool isIndexable;
|
||||
@override
|
||||
final SnAttachment? ref;
|
||||
@override
|
||||
final int? refId;
|
||||
@override
|
||||
final SnAttachmentPool? pool;
|
||||
@override
|
||||
final int poolId;
|
||||
final int? poolId;
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
@@ -634,6 +665,15 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
final int? compressedId;
|
||||
@override
|
||||
final SnAttachment? compressed;
|
||||
final List<SnAttachmentBoost> _boosts;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<SnAttachmentBoost> get boosts {
|
||||
if (_boosts is EqualUnmodifiableListView) return _boosts;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_boosts);
|
||||
}
|
||||
|
||||
final Map<String, dynamic> _usermeta;
|
||||
@override
|
||||
@JsonKey()
|
||||
@@ -654,7 +694,7 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, usermeta: $usermeta, metadata: $metadata)';
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -691,6 +731,8 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
other.isAnalyzed == isAnalyzed) &&
|
||||
(identical(other.isSelfRef, isSelfRef) ||
|
||||
other.isSelfRef == isSelfRef) &&
|
||||
(identical(other.isIndexable, isIndexable) ||
|
||||
other.isIndexable == isIndexable) &&
|
||||
(identical(other.ref, ref) || other.ref == ref) &&
|
||||
(identical(other.refId, refId) || other.refId == refId) &&
|
||||
(identical(other.pool, pool) || other.pool == pool) &&
|
||||
@@ -705,6 +747,7 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
other.compressedId == compressedId) &&
|
||||
(identical(other.compressed, compressed) ||
|
||||
other.compressed == compressed) &&
|
||||
const DeepCollectionEquality().equals(other._boosts, _boosts) &&
|
||||
const DeepCollectionEquality().equals(other._usermeta, _usermeta) &&
|
||||
const DeepCollectionEquality().equals(other._metadata, _metadata));
|
||||
}
|
||||
@@ -731,6 +774,7 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
cleanedAt,
|
||||
isAnalyzed,
|
||||
isSelfRef,
|
||||
isIndexable,
|
||||
ref,
|
||||
refId,
|
||||
pool,
|
||||
@@ -740,6 +784,7 @@ class _$SnAttachmentImpl extends _SnAttachment {
|
||||
thumbnail,
|
||||
compressedId,
|
||||
compressed,
|
||||
const DeepCollectionEquality().hash(_boosts),
|
||||
const DeepCollectionEquality().hash(_usermeta),
|
||||
const DeepCollectionEquality().hash(_metadata)
|
||||
]);
|
||||
@@ -780,15 +825,17 @@ abstract class _SnAttachment extends SnAttachment {
|
||||
required final DateTime? cleanedAt,
|
||||
required final bool isAnalyzed,
|
||||
required final bool isSelfRef,
|
||||
required final bool isIndexable,
|
||||
required final SnAttachment? ref,
|
||||
required final int? refId,
|
||||
required final SnAttachmentPool? pool,
|
||||
required final int poolId,
|
||||
required final int? poolId,
|
||||
required final int accountId,
|
||||
final int? thumbnailId,
|
||||
final SnAttachment? thumbnail,
|
||||
final int? compressedId,
|
||||
final SnAttachment? compressed,
|
||||
final List<SnAttachmentBoost> boosts,
|
||||
final Map<String, dynamic> usermeta,
|
||||
final Map<String, dynamic> metadata}) = _$SnAttachmentImpl;
|
||||
const _SnAttachment._() : super._();
|
||||
@@ -833,13 +880,15 @@ abstract class _SnAttachment extends SnAttachment {
|
||||
@override
|
||||
bool get isSelfRef;
|
||||
@override
|
||||
bool get isIndexable;
|
||||
@override
|
||||
SnAttachment? get ref;
|
||||
@override
|
||||
int? get refId;
|
||||
@override
|
||||
SnAttachmentPool? get pool;
|
||||
@override
|
||||
int get poolId;
|
||||
int? get poolId;
|
||||
@override
|
||||
int get accountId;
|
||||
@override
|
||||
@@ -851,6 +900,8 @@ abstract class _SnAttachment extends SnAttachment {
|
||||
@override
|
||||
SnAttachment? get compressed;
|
||||
@override
|
||||
List<SnAttachmentBoost> get boosts;
|
||||
@override
|
||||
Map<String, dynamic> get usermeta;
|
||||
@override
|
||||
Map<String, dynamic> get metadata;
|
||||
@@ -1654,3 +1705,570 @@ abstract class _SnAttachmentPool implements SnAttachmentPool {
|
||||
_$$SnAttachmentPoolImplCopyWith<_$SnAttachmentPoolImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnAttachmentDestination _$SnAttachmentDestinationFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return _SnAttachmentDestination.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnAttachmentDestination {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
String get type => throw _privateConstructorUsedError;
|
||||
String get label => throw _privateConstructorUsedError;
|
||||
String get region => throw _privateConstructorUsedError;
|
||||
bool get isBoost => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnAttachmentDestination to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnAttachmentDestination
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnAttachmentDestinationCopyWith<SnAttachmentDestination> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnAttachmentDestinationCopyWith<$Res> {
|
||||
factory $SnAttachmentDestinationCopyWith(SnAttachmentDestination value,
|
||||
$Res Function(SnAttachmentDestination) then) =
|
||||
_$SnAttachmentDestinationCopyWithImpl<$Res, SnAttachmentDestination>;
|
||||
@useResult
|
||||
$Res call({int id, String type, String label, String region, bool isBoost});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnAttachmentDestinationCopyWithImpl<$Res,
|
||||
$Val extends SnAttachmentDestination>
|
||||
implements $SnAttachmentDestinationCopyWith<$Res> {
|
||||
_$SnAttachmentDestinationCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnAttachmentDestination
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? type = null,
|
||||
Object? label = null,
|
||||
Object? region = null,
|
||||
Object? isBoost = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
region: null == region
|
||||
? _value.region
|
||||
: region // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isBoost: null == isBoost
|
||||
? _value.isBoost
|
||||
: isBoost // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnAttachmentDestinationImplCopyWith<$Res>
|
||||
implements $SnAttachmentDestinationCopyWith<$Res> {
|
||||
factory _$$SnAttachmentDestinationImplCopyWith(
|
||||
_$SnAttachmentDestinationImpl value,
|
||||
$Res Function(_$SnAttachmentDestinationImpl) then) =
|
||||
__$$SnAttachmentDestinationImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int id, String type, String label, String region, bool isBoost});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnAttachmentDestinationImplCopyWithImpl<$Res>
|
||||
extends _$SnAttachmentDestinationCopyWithImpl<$Res,
|
||||
_$SnAttachmentDestinationImpl>
|
||||
implements _$$SnAttachmentDestinationImplCopyWith<$Res> {
|
||||
__$$SnAttachmentDestinationImplCopyWithImpl(
|
||||
_$SnAttachmentDestinationImpl _value,
|
||||
$Res Function(_$SnAttachmentDestinationImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnAttachmentDestination
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? type = null,
|
||||
Object? label = null,
|
||||
Object? region = null,
|
||||
Object? isBoost = null,
|
||||
}) {
|
||||
return _then(_$SnAttachmentDestinationImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
type: null == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _value.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
region: null == region
|
||||
? _value.region
|
||||
: region // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isBoost: null == isBoost
|
||||
? _value.isBoost
|
||||
: isBoost // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnAttachmentDestinationImpl implements _SnAttachmentDestination {
|
||||
const _$SnAttachmentDestinationImpl(
|
||||
{this.id = 0,
|
||||
required this.type,
|
||||
required this.label,
|
||||
required this.region,
|
||||
required this.isBoost});
|
||||
|
||||
factory _$SnAttachmentDestinationImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnAttachmentDestinationImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final int id;
|
||||
@override
|
||||
final String type;
|
||||
@override
|
||||
final String label;
|
||||
@override
|
||||
final String region;
|
||||
@override
|
||||
final bool isBoost;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAttachmentDestination(id: $id, type: $type, label: $label, region: $region, isBoost: $isBoost)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnAttachmentDestinationImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.region, region) || other.region == region) &&
|
||||
(identical(other.isBoost, isBoost) || other.isBoost == isBoost));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, id, type, label, region, isBoost);
|
||||
|
||||
/// Create a copy of SnAttachmentDestination
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnAttachmentDestinationImplCopyWith<_$SnAttachmentDestinationImpl>
|
||||
get copyWith => __$$SnAttachmentDestinationImplCopyWithImpl<
|
||||
_$SnAttachmentDestinationImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnAttachmentDestinationImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnAttachmentDestination implements SnAttachmentDestination {
|
||||
const factory _SnAttachmentDestination(
|
||||
{final int id,
|
||||
required final String type,
|
||||
required final String label,
|
||||
required final String region,
|
||||
required final bool isBoost}) = _$SnAttachmentDestinationImpl;
|
||||
|
||||
factory _SnAttachmentDestination.fromJson(Map<String, dynamic> json) =
|
||||
_$SnAttachmentDestinationImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
String get type;
|
||||
@override
|
||||
String get label;
|
||||
@override
|
||||
String get region;
|
||||
@override
|
||||
bool get isBoost;
|
||||
|
||||
/// Create a copy of SnAttachmentDestination
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnAttachmentDestinationImplCopyWith<_$SnAttachmentDestinationImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnAttachmentBoost _$SnAttachmentBoostFromJson(Map<String, dynamic> json) {
|
||||
return _SnAttachmentBoost.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnAttachmentBoost {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get deletedAt => throw _privateConstructorUsedError;
|
||||
int get status => throw _privateConstructorUsedError;
|
||||
int get destination => throw _privateConstructorUsedError;
|
||||
int get attachmentId => throw _privateConstructorUsedError;
|
||||
SnAttachment get attachment => throw _privateConstructorUsedError;
|
||||
int get account => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnAttachmentBoost to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnAttachmentBoost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnAttachmentBoostCopyWith<SnAttachmentBoost> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnAttachmentBoostCopyWith<$Res> {
|
||||
factory $SnAttachmentBoostCopyWith(
|
||||
SnAttachmentBoost value, $Res Function(SnAttachmentBoost) then) =
|
||||
_$SnAttachmentBoostCopyWithImpl<$Res, SnAttachmentBoost>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
int status,
|
||||
int destination,
|
||||
int attachmentId,
|
||||
SnAttachment attachment,
|
||||
int account});
|
||||
|
||||
$SnAttachmentCopyWith<$Res> get attachment;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnAttachmentBoostCopyWithImpl<$Res, $Val extends SnAttachmentBoost>
|
||||
implements $SnAttachmentBoostCopyWith<$Res> {
|
||||
_$SnAttachmentBoostCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnAttachmentBoost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? status = null,
|
||||
Object? destination = null,
|
||||
Object? attachmentId = null,
|
||||
Object? attachment = null,
|
||||
Object? account = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
status: null == status
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
destination: null == destination
|
||||
? _value.destination
|
||||
: destination // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
attachmentId: null == attachmentId
|
||||
? _value.attachmentId
|
||||
: attachmentId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
attachment: null == attachment
|
||||
? _value.attachment
|
||||
: attachment // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment,
|
||||
account: null == account
|
||||
? _value.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
/// Create a copy of SnAttachmentBoost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAttachmentCopyWith<$Res> get attachment {
|
||||
return $SnAttachmentCopyWith<$Res>(_value.attachment, (value) {
|
||||
return _then(_value.copyWith(attachment: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnAttachmentBoostImplCopyWith<$Res>
|
||||
implements $SnAttachmentBoostCopyWith<$Res> {
|
||||
factory _$$SnAttachmentBoostImplCopyWith(_$SnAttachmentBoostImpl value,
|
||||
$Res Function(_$SnAttachmentBoostImpl) then) =
|
||||
__$$SnAttachmentBoostImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
int status,
|
||||
int destination,
|
||||
int attachmentId,
|
||||
SnAttachment attachment,
|
||||
int account});
|
||||
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res> get attachment;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnAttachmentBoostImplCopyWithImpl<$Res>
|
||||
extends _$SnAttachmentBoostCopyWithImpl<$Res, _$SnAttachmentBoostImpl>
|
||||
implements _$$SnAttachmentBoostImplCopyWith<$Res> {
|
||||
__$$SnAttachmentBoostImplCopyWithImpl(_$SnAttachmentBoostImpl _value,
|
||||
$Res Function(_$SnAttachmentBoostImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnAttachmentBoost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? status = null,
|
||||
Object? destination = null,
|
||||
Object? attachmentId = null,
|
||||
Object? attachment = null,
|
||||
Object? account = null,
|
||||
}) {
|
||||
return _then(_$SnAttachmentBoostImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
status: null == status
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
destination: null == destination
|
||||
? _value.destination
|
||||
: destination // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
attachmentId: null == attachmentId
|
||||
? _value.attachmentId
|
||||
: attachmentId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
attachment: null == attachment
|
||||
? _value.attachment
|
||||
: attachment // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment,
|
||||
account: null == account
|
||||
? _value.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnAttachmentBoostImpl implements _SnAttachmentBoost {
|
||||
const _$SnAttachmentBoostImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.status,
|
||||
required this.destination,
|
||||
required this.attachmentId,
|
||||
required this.attachment,
|
||||
required this.account});
|
||||
|
||||
factory _$SnAttachmentBoostImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnAttachmentBoostImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final int status;
|
||||
@override
|
||||
final int destination;
|
||||
@override
|
||||
final int attachmentId;
|
||||
@override
|
||||
final SnAttachment attachment;
|
||||
@override
|
||||
final int account;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAttachmentBoost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, status: $status, destination: $destination, attachmentId: $attachmentId, attachment: $attachment, account: $account)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnAttachmentBoostImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.status, status) || other.status == status) &&
|
||||
(identical(other.destination, destination) ||
|
||||
other.destination == destination) &&
|
||||
(identical(other.attachmentId, attachmentId) ||
|
||||
other.attachmentId == attachmentId) &&
|
||||
(identical(other.attachment, attachment) ||
|
||||
other.attachment == attachment) &&
|
||||
(identical(other.account, account) || other.account == account));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, status, destination, attachmentId, attachment, account);
|
||||
|
||||
/// Create a copy of SnAttachmentBoost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith =>
|
||||
__$$SnAttachmentBoostImplCopyWithImpl<_$SnAttachmentBoostImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnAttachmentBoostImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnAttachmentBoost implements SnAttachmentBoost {
|
||||
const factory _SnAttachmentBoost(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final DateTime? deletedAt,
|
||||
required final int status,
|
||||
required final int destination,
|
||||
required final int attachmentId,
|
||||
required final SnAttachment attachment,
|
||||
required final int account}) = _$SnAttachmentBoostImpl;
|
||||
|
||||
factory _SnAttachmentBoost.fromJson(Map<String, dynamic> json) =
|
||||
_$SnAttachmentBoostImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
DateTime? get deletedAt;
|
||||
@override
|
||||
int get status;
|
||||
@override
|
||||
int get destination;
|
||||
@override
|
||||
int get attachmentId;
|
||||
@override
|
||||
SnAttachment get attachment;
|
||||
@override
|
||||
int get account;
|
||||
|
||||
/// Create a copy of SnAttachmentBoost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
: DateTime.parse(json['cleaned_at'] as String),
|
||||
isAnalyzed: json['is_analyzed'] as bool,
|
||||
isSelfRef: json['is_self_ref'] as bool,
|
||||
isIndexable: json['is_indexable'] as bool,
|
||||
ref: json['ref'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['ref'] as Map<String, dynamic>),
|
||||
@@ -37,7 +38,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
pool: json['pool'] == null
|
||||
? null
|
||||
: 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(),
|
||||
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||
thumbnail: json['thumbnail'] == null
|
||||
@@ -47,6 +48,11 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
||||
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 {},
|
||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||
);
|
||||
@@ -71,6 +77,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
||||
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
||||
'is_analyzed': instance.isAnalyzed,
|
||||
'is_self_ref': instance.isSelfRef,
|
||||
'is_indexable': instance.isIndexable,
|
||||
'ref': instance.ref?.toJson(),
|
||||
'ref_id': instance.refId,
|
||||
'pool': instance.pool?.toJson(),
|
||||
@@ -80,6 +87,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
'compressed_id': instance.compressedId,
|
||||
'compressed': instance.compressed?.toJson(),
|
||||
'boosts': instance.boosts.map((e) => e.toJson()).toList(),
|
||||
'usermeta': instance.usermeta,
|
||||
'metadata': instance.metadata,
|
||||
};
|
||||
@@ -159,3 +167,54 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
|
||||
'config': instance.config,
|
||||
'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,
|
||||
};
|
||||
|
@@ -10,8 +10,8 @@ import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class AttachmentInputDialog extends StatefulWidget {
|
||||
final String? title;
|
||||
|
||||
const AttachmentInputDialog({super.key, required this.title});
|
||||
final bool? analyzeNow;
|
||||
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||
|
||||
@override
|
||||
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||
@@ -53,6 +53,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
_thumbnailFile!.path,
|
||||
'interactive',
|
||||
null,
|
||||
analyzeNow: widget.analyzeNow ?? false,
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, attachment);
|
||||
@@ -77,7 +78,8 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
controller: _randomIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
border: const UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -18,9 +20,11 @@ import 'package:uuid/uuid.dart';
|
||||
class AttachmentItem extends StatelessWidget {
|
||||
final SnAttachment? data;
|
||||
final String? heroTag;
|
||||
final BoxFit fit;
|
||||
|
||||
const AttachmentItem({
|
||||
super.key,
|
||||
this.fit = BoxFit.cover,
|
||||
required this.data,
|
||||
required this.heroTag,
|
||||
});
|
||||
@@ -41,7 +45,7 @@ class AttachmentItem extends StatelessWidget {
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data!.rid),
|
||||
key: Key('attachment-${data!.rid}-$tag'),
|
||||
fit: BoxFit.cover,
|
||||
fit: fit,
|
||||
),
|
||||
);
|
||||
case 'video':
|
||||
@@ -62,14 +66,12 @@ class AttachmentItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (data!.contentRating > 0) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return _AttachmentItemSensitiveBlur(
|
||||
isCompact: constraints.maxHeight < 360,
|
||||
child: _buildContent(context),
|
||||
);
|
||||
}
|
||||
);
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return _AttachmentItemSensitiveBlur(
|
||||
isCompact: constraints.maxHeight < 360,
|
||||
child: _buildContent(context),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return _buildContent(context);
|
||||
@@ -176,6 +178,7 @@ class _AttachmentItemContentVideo extends StatefulWidget {
|
||||
|
||||
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
|
||||
bool _showContent = false;
|
||||
bool _showOriginal = false;
|
||||
|
||||
Player? _videoPlayer;
|
||||
VideoController? _videoController;
|
||||
@@ -184,15 +187,29 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
setState(() => _showContent = true);
|
||||
MediaKit.ensureInitialized();
|
||||
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();
|
||||
_videoController = VideoController(_videoPlayer!);
|
||||
_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
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showOriginal = widget.data.compressedId == null;
|
||||
if (widget.isAutoload) _startLoad();
|
||||
}
|
||||
|
||||
@@ -297,9 +314,45 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
);
|
||||
}
|
||||
|
||||
return Video(
|
||||
controller: _videoController!,
|
||||
aspectRatio: ratio,
|
||||
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!,
|
||||
aspectRatio: ratio,
|
||||
controls:
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -4,8 +4,8 @@ import 'package:collection/collection.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
@@ -14,19 +14,23 @@ import 'package:uuid/uuid.dart';
|
||||
class AttachmentList extends StatefulWidget {
|
||||
final List<SnAttachment?> data;
|
||||
final bool bordered;
|
||||
final bool gridded;
|
||||
final bool noGrow;
|
||||
final bool isFlatted;
|
||||
final BoxFit fit;
|
||||
final double? maxHeight;
|
||||
final EdgeInsets? listPadding;
|
||||
final double? minWidth;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const AttachmentList({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.bordered = false,
|
||||
this.gridded = false,
|
||||
this.noGrow = false,
|
||||
this.isFlatted = false,
|
||||
this.fit = BoxFit.cover,
|
||||
this.maxHeight,
|
||||
this.listPadding,
|
||||
this.minWidth,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
||||
@@ -41,8 +45,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
(_) => const Uuid().v4(),
|
||||
);
|
||||
|
||||
static const double kAttachmentMaxWidth = 640;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
@@ -51,9 +53,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
final constraints = BoxConstraints(
|
||||
minWidth: 80,
|
||||
maxHeight: widget.maxHeight ?? double.infinity,
|
||||
maxWidth: layoutConstraints.maxWidth - 20,
|
||||
minWidth: widget.minWidth ?? 80,
|
||||
maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
|
||||
);
|
||||
|
||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||
@@ -67,97 +68,90 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
.toDouble();
|
||||
|
||||
return Container(
|
||||
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? constraints.copyWith(
|
||||
maxWidth: math.min(
|
||||
constraints.maxWidth,
|
||||
kAttachmentMaxWidth,
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
constraints: constraints,
|
||||
child: GestureDetector(
|
||||
child: AspectRatio(
|
||||
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) {
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => AspectRatio(
|
||||
aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
if (widget.gridded) {
|
||||
return Padding(
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: StaggeredGrid.count(
|
||||
crossAxisCount: math.min(widget.data.length, 2),
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,7 +217,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
padding: widget.listPadding,
|
||||
padding: widget.padding,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
|
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal file
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal 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()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@@ -159,9 +159,10 @@ class ChatMessage extends StatelessWidget {
|
||||
AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
bordered: true,
|
||||
gridded: true,
|
||||
noGrow: true,
|
||||
maxHeight: 520,
|
||||
listPadding: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
),
|
||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
|
||||
],
|
||||
|
@@ -83,6 +83,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
media.toFile()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
analyzeNow: media.type == SnMediaType.image,
|
||||
onProgress: (progress) {
|
||||
// Calculate overall progress for attachments
|
||||
setState(() {
|
||||
|
@@ -18,7 +18,6 @@ import 'package:screenshot/screenshot.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.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/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
@@ -85,7 +84,6 @@ class PostItem extends StatelessWidget {
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
||||
Provider<SnLinkPreviewProvider>(create: (_) => context.read()),
|
||||
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
||||
],
|
||||
child: ResponsiveBreakpoints.builder(
|
||||
@@ -253,8 +251,11 @@ class PostItem extends StatelessWidget {
|
||||
AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
bordered: true,
|
||||
maxHeight: 560,
|
||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
gridded: true,
|
||||
maxHeight: showFullPost ? null : 480,
|
||||
minWidth: 640,
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
if (data.body['content'] != null)
|
||||
LinkPreviewWidget(
|
||||
@@ -334,17 +335,12 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
_PostQuoteContent(
|
||||
child: data.repostTo!,
|
||||
isRelativeDate: false,
|
||||
isFlatted: true,
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
||||
AttachmentList(
|
||||
StyledWidget(AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
isFlatted: true,
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
if (data.body['content'] != null)
|
||||
LinkPreviewWidget(
|
||||
text: data.body['content'],
|
||||
).padding(horizontal: 4),
|
||||
gridded: true,
|
||||
)).padding(horizontal: 16, bottom: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -890,11 +886,9 @@ class _PostContentBody extends StatelessWidget {
|
||||
class _PostQuoteContent extends StatelessWidget {
|
||||
final SnPost child;
|
||||
final bool isRelativeDate;
|
||||
final bool isFlatted;
|
||||
|
||||
const _PostQuoteContent({
|
||||
this.isRelativeDate = true,
|
||||
this.isFlatted = false,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@@ -936,12 +930,15 @@ class _PostQuoteContent extends StatelessWidget {
|
||||
),
|
||||
child: AttachmentList(
|
||||
data: child.preload!.attachments!,
|
||||
isFlatted: isFlatted,
|
||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
maxHeight: 360,
|
||||
minWidth: 640,
|
||||
fit: BoxFit.contain,
|
||||
gridded: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
).padding(
|
||||
top: 8,
|
||||
bottom: (child.preload?.attachments?.length ?? 0) > 1 ? 12 : 0,
|
||||
bottom: 12,
|
||||
)
|
||||
else
|
||||
const Gap(8),
|
||||
|
@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:material_symbols_icons/symbols.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/widgets/attachment/attachment_input.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/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@@ -92,18 +94,23 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
if (thumbnail == null) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final newAttach = await attach.updateOne(
|
||||
attachments[idx].attachment!.id,
|
||||
thumbnailId: thumbnail.id,
|
||||
);
|
||||
|
||||
onUpdate!(idx, PostWriteMedia(newAttach));
|
||||
try {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final newAttach = await attach.updateOne(
|
||||
attachments[idx].attachment!,
|
||||
thumbnailId: thumbnail.id,
|
||||
);
|
||||
onUpdate!(idx, PostWriteMedia(newAttach));
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
||||
@@ -123,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 {
|
||||
final result = await showDialog<PostWriteMedia?>(
|
||||
context: context,
|
||||
@@ -145,6 +169,14 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
_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)
|
||||
MenuItem(
|
||||
label: 'attachmentSetThumbnail'.tr(),
|
||||
@@ -293,64 +325,137 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: switch (media.type) {
|
||||
SnMediaType.image => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Image(
|
||||
image: media.getImageProvider(
|
||||
context,
|
||||
width: (constraints.maxWidth * devicePixelRatio).round(),
|
||||
height: (constraints.maxHeight * devicePixelRatio).round(),
|
||||
)!,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}),
|
||||
),
|
||||
SnMediaType.video => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
child: Row(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: switch (media.type) {
|
||||
SnMediaType.image => LayoutBuilder(builder: (context, constraints) {
|
||||
return Image(
|
||||
image: media.getImageProvider(
|
||||
context,
|
||||
width: (constraints.maxWidth * devicePixelRatio).round(),
|
||||
height: (constraints.maxHeight * devicePixelRatio).round(),
|
||||
)!,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}),
|
||||
SnMediaType.video => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.videocam, color: Colors.white, shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
SnMediaType.audio => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
_ => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
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: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.videocam, color: Colors.white, shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
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),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
SnMediaType.audio => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: const Icon(Symbols.docs).center(),
|
||||
),
|
||||
},
|
||||
).padding(horizontal: 12, vertical: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -7,7 +7,6 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
// Keep this import to make the web image render work
|
||||
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
@@ -34,64 +33,54 @@ class UniversalImage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
|
||||
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
|
||||
final quality = filterQuality ?? context.read<ConfigProvider>().imageQuality;
|
||||
|
||||
return Image(
|
||||
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality,
|
||||
image: kIsWeb
|
||||
? UniversalImage.provider(url)
|
||||
: ResizeImage(
|
||||
UniversalImage.provider(url),
|
||||
width: resizeWidth?.round(),
|
||||
height: resizeHeight?.round(),
|
||||
policy: ResizeImagePolicy.fit,
|
||||
),
|
||||
return ExtendedImage.network(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
loadingBuilder: noProgressIndicator
|
||||
? null
|
||||
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: TweenAnimationBuilder(
|
||||
tween: Tween(
|
||||
begin: 0,
|
||||
end: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
: 0,
|
||||
cache: true,
|
||||
compressionRatio: kIsWeb ? 1 : switch(quality) {
|
||||
FilterQuality.high => 1,
|
||||
FilterQuality.medium => 0.75,
|
||||
FilterQuality.low => 0.5,
|
||||
FilterQuality.none => 0.25,
|
||||
},
|
||||
filterQuality: quality,
|
||||
enableLoadState: true,
|
||||
retries: 3,
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
if (state.extendedImageLoadState == LoadState.completed) {
|
||||
return state.completedWidget;
|
||||
} else if (state.extendedImageLoadState == LoadState.failed) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
Text(
|
||||
state.lastException.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: noErrorWidget
|
||||
? null
|
||||
: (context, error, stackTrace) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
).center(),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
).center(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: state.loadingProgress != null
|
||||
? state.loadingProgress!.cumulativeBytesLoaded / state.loadingProgress!.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,9 +88,10 @@ class UniversalImage extends StatelessWidget {
|
||||
// This place used to use network image or cached network image depending on the platform.
|
||||
// But now the cached network image is working on every platform.
|
||||
// So we just use it now.
|
||||
return CachedNetworkImageProvider(
|
||||
return ExtendedNetworkImageProvider(
|
||||
url,
|
||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||
cache: true,
|
||||
retries: 3,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,6 @@ import path_provider_foundation
|
||||
import screen_brightness_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
import video_compress
|
||||
import wakelock_plus
|
||||
@@ -53,7 +52,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
|
@@ -134,7 +134,7 @@ PODS:
|
||||
- GoogleUtilities/Privacy
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.3.3):
|
||||
- livekit_client (2.3.4):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@@ -165,9 +165,6 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- video_compress (0.3.0):
|
||||
@@ -201,7 +198,6 @@ DEPENDENCIES:
|
||||
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
@@ -271,8 +267,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
video_compress:
|
||||
@@ -304,7 +298,7 @@ SPEC CHECKSUMS:
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||
livekit_client: 8b1b90a6f2445d127a018ce93cc8cf6d8ab62982
|
||||
livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
@@ -317,7 +311,6 @@ SPEC CHECKSUMS:
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
|
116
pubspec.lock
116
pubspec.lock
@@ -182,30 +182,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.3"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
cassowary:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -454,6 +430,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
extended_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_image
|
||||
sha256: "93890a88d89ce017789f6c031c32ad8d2c685f1a5c25c169550746d973ca5e44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.9"
|
||||
extended_image_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: extended_image_library
|
||||
sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.5"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -643,14 +635,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
flutter_colorpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -890,6 +874,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
http_client_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_client_helper
|
||||
sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1086,10 +1078,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: a3ff529fe6745ee40cdedcd021d81c4a6ad946dd495e782596f2856eeeabc739
|
||||
sha256: "7cdeb3eaeec7fb70a4cf88d9caabccbef9e3bd5f0b23c086320bc5c9acb2770b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.3.4"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1250,14 +1242,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1546,14 +1530,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
safe_local_storage:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1767,46 +1743,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4+6"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.2.1+40
|
||||
version: 2.2.1+42
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -53,7 +53,6 @@ dependencies:
|
||||
markdown: ^7.2.2
|
||||
flutter_markdown: ^0.7.4+1
|
||||
url_launcher: ^6.3.1
|
||||
cached_network_image: ^3.4.1
|
||||
flutter_animate: ^4.5.0
|
||||
syntax_highlight: ^0.4.0
|
||||
google_fonts: ^6.2.1
|
||||
@@ -116,6 +115,7 @@ dependencies:
|
||||
flutter_webrtc: ^0.12.5+hotfix.1
|
||||
slide_countdown: ^2.0.2
|
||||
video_compress: ^3.1.3
|
||||
extended_image: ^9.0.9
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user