Compare commits

...

3 Commits

11 changed files with 320 additions and 114 deletions

View File

@ -298,6 +298,18 @@
"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": "Has compressed copy",
"notification": "Notification",
"notificationUnreadCount": {
"zero": "All notifications read",
@ -489,7 +501,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 +525,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"
}

View File

@ -296,6 +296,18 @@
"attachmentInputDialog": "上传附件",
"attachmentInputUseRandomId": "使用访问 ID",
"attachmentInputNew": "新上传附件",
"waitingForUpload": "等待上传",
"attachmentVideoCompressHint": "压缩一份视频的副本",
"attachmentVideoCompressHintDescription": "你想上传压缩视频 {} 的副本吗?它将帮助你的观众快速预览视频,并且他们仍然可以观看原始视频。这将会在在你的设备上处理视频,所以需要一些时间,所以请耐心等待。",
"attachmentCompressQuality": "压缩质量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默认",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。",
"attachmentUploaded": "已上传",
"attachmentPending": "未上传",
"attachmentCopyCompressed": "有压缩副本",
"notification": "通知",
"notificationUnreadCount": {
"zero": "无未读通知",
@ -511,12 +523,5 @@
"postCategoryKnowledge": "知识",
"postCategoryLiterature": "文学",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分类",
"waitingForUpload": "等待上传",
"attachmentCompressQuality": "压缩质量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默认",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。"
"postCategoryUncategorized": "未分类"
}

View File

@ -296,6 +296,18 @@
"attachmentInputDialog": "上傳附件",
"attachmentInputUseRandomId": "使用訪問 ID",
"attachmentInputNew": "新上傳附件",
"waitingForUpload": "等待上傳",
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
"attachmentUploaded": "已上傳",
"attachmentPending": "未上傳",
"attachmentCopyCompressed": "有壓縮副本",
"notification": "通知",
"notificationUnreadCount": {
"zero": "無未讀通知",
@ -511,12 +523,5 @@
"postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類",
"waitingForUpload": "等待上傳",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
"postCategoryUncategorized": "未分類"
}

View File

@ -296,6 +296,18 @@
"attachmentInputDialog": "上傳附件",
"attachmentInputUseRandomId": "使用訪問 ID",
"attachmentInputNew": "新上傳附件",
"waitingForUpload": "等待上傳",
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
"attachmentUploaded": "已上傳",
"attachmentPending": "未上傳",
"attachmentCopyCompressed": "有壓縮副本",
"notification": "通知",
"notificationUnreadCount": {
"zero": "無未讀通知",
@ -511,12 +523,5 @@
"postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類",
"waitingForUpload": "等待上傳",
"attachmentCompressQuality": "壓縮質量",
"attachmentCompressQualityHighest": "最高",
"attachmentCompressQualityDefault": "默認",
"attachmentCompressQualityMedium": "中等",
"attachmentCompressQualityLow": "低",
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
"postCategoryUncategorized": "未分類"
}

View File

@ -2,6 +2,7 @@ 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';
@ -14,6 +15,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;
@ -229,7 +231,7 @@ 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(
@ -244,15 +246,52 @@ class PostWriteController extends ChangeNotifier {
media.toFile()!,
place.$1,
place.$2,
onProgress: (progress) {
progress = progress;
onProgress: (value) {
progress = value;
notifyListeners();
},
);
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
final compressedAttachment = await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) {
await attach.updateOne(item, compressedId: compressedAttachment.id);
}
}
return item;
}
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
if (media.type != SnMediaType.video) return null;
if (media.file == null) return null;
if (VideoCompress.isCompressing) return null;
final confirm = await context.showConfirmDialog(
'attachmentVideoCompressHint'.tr(),
'attachmentVideoCompressHintDescription'.tr(args: [media.file!.name]),
);
if (!confirm) return null;
progress = null;
notifyListeners();
final mediaInfo = await VideoCompress.compressVideo(
media.file!.path,
quality: VideoQuality.LowQuality,
frameRate: 30,
deleteOrigin: false,
);
if (mediaInfo == null) return null;
if (!context.mounted) return null;
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
return compressedAttachment;
}
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
if (isBusy) return;
@ -301,13 +340,20 @@ class PostWriteController extends ChangeNotifier {
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();
},
);
if (media.type == SnMediaType.video && context.mounted) {
final compressedAttachment = await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) {
await attach.updateOne(item, compressedId: compressedAttachment.id);
}
}
progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
attachments[i] = PostWriteMedia(item);
notifyListeners();

View File

@ -178,7 +178,7 @@ class SnAttachmentProvider {
Function(double progress)? onProgress,
}) async {
final Map<String, dynamic> chunks = place.fileChunks;
var currentTask = 0;
var completedTasks = 0;
final queue = Queue<Future<void>>();
final activeTasks = <Future<void>>[];
@ -199,13 +199,13 @@ class SnAttachmentProvider {
place.rid,
entry.key,
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 +233,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);
}

View File

@ -34,6 +34,7 @@ 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,

View File

@ -38,6 +38,7 @@ 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;
@ -85,6 +86,7 @@ abstract class $SnAttachmentCopyWith<$Res> {
DateTime? cleanedAt,
bool isAnalyzed,
bool isSelfRef,
bool isIndexable,
SnAttachment? ref,
int? refId,
SnAttachmentPool? pool,
@ -136,6 +138,7 @@ 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,
@ -221,6 +224,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
@ -352,6 +359,7 @@ abstract class _$$SnAttachmentImplCopyWith<$Res>
DateTime? cleanedAt,
bool isAnalyzed,
bool isSelfRef,
bool isIndexable,
SnAttachment? ref,
int? refId,
SnAttachmentPool? pool,
@ -405,6 +413,7 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
Object? cleanedAt = freezed,
Object? isAnalyzed = null,
Object? isSelfRef = null,
Object? isIndexable = null,
Object? ref = freezed,
Object? refId = freezed,
Object? pool = freezed,
@ -490,6 +499,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
@ -560,6 +573,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,
@ -617,6 +631,8 @@ class _$SnAttachmentImpl extends _SnAttachment {
@override
final bool isSelfRef;
@override
final bool isIndexable;
@override
final SnAttachment? ref;
@override
final int? refId;
@ -654,7 +670,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, usermeta: $usermeta, metadata: $metadata)';
}
@override
@ -691,6 +707,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) &&
@ -731,6 +749,7 @@ class _$SnAttachmentImpl extends _SnAttachment {
cleanedAt,
isAnalyzed,
isSelfRef,
isIndexable,
ref,
refId,
pool,
@ -780,6 +799,7 @@ 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,
@ -833,6 +853,8 @@ abstract class _SnAttachment extends SnAttachment {
@override
bool get isSelfRef;
@override
bool get isIndexable;
@override
SnAttachment? get ref;
@override
int? get refId;

View File

@ -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>),
@ -71,6 +72,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(),

View File

@ -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';
@ -62,14 +64,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 +176,7 @@ class _AttachmentItemContentVideo extends StatefulWidget {
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
bool _showContent = false;
bool _showOriginal = false;
Player? _videoPlayer;
VideoController? _videoController;
@ -184,15 +185,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 +312,43 @@ 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: _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,
),
),
);
}

View File

@ -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';
@ -99,7 +100,7 @@ class PostMediaPendingList extends StatelessWidget {
final attach = context.read<SnAttachmentProvider>();
final newAttach = await attach.updateOne(
attachments[idx].attachment!.id,
attachments[idx].attachment!,
thumbnailId: thumbnail.id,
);
@ -293,64 +294,129 @@ 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!.compressedId != null)
Row(
children: [
Icon(Symbols.bolt, 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),
],
),
),
);