From 7ed508e2bb51a2d2121e078e695018ceac861cc4 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 10 Feb 2025 00:44:52 +0800 Subject: [PATCH] :sparkles: Video post --- api/Paperclip/Stickers/Create Sticker.bru | 8 +- assets/translations/en-US.json | 4 +- assets/translations/zh-CN.json | 4 +- ios/Podfile.lock | 11 +- lib/controllers/post_write_controller.dart | 8 ++ lib/providers/post.dart | 8 ++ lib/screens/explore.dart | 26 +++- lib/screens/post/post_editor.dart | 120 ++++++++++++++++++ lib/screens/post/post_search.dart | 1 - lib/types/post.dart | 1 + lib/types/post.freezed.dart | 56 +++++++- lib/types/post.g.dart | 4 + lib/widgets/attachment/attachment_input.dart | 81 ++++++++---- lib/widgets/post/post_item.dart | 29 +++++ lib/widgets/post/post_media_pending_list.dart | 1 + macos/Podfile.lock | 11 +- pubspec.lock | 48 ++++--- 17 files changed, 345 insertions(+), 76 deletions(-) diff --git a/api/Paperclip/Stickers/Create Sticker.bru b/api/Paperclip/Stickers/Create Sticker.bru index a5c4591..98b91b8 100644 --- a/api/Paperclip/Stickers/Create Sticker.bru +++ b/api/Paperclip/Stickers/Create Sticker.bru @@ -12,9 +12,9 @@ post { body:json { { - "alias": "Meltdown", - "name": "Meltdown", - "attachment_id": "IpDPHEbWDDCbBofX", - "pack_id": 4 + "alias": "BaLoading", + "name": "BaLoading", + "attachment_id": "2JCI2uh21mKkfk9P", + "pack_id": 3 } } diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 9f31f07..040de1d 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -155,6 +155,7 @@ "writePostTypeStory": "Post a story", "writePostTypeArticle": "Write an article", "writePostTypeQuestion": "Ask a question", + "writePostTypeVideo": "Post a video", "fieldPostPublisher": "Post publisher", "fieldPostContent": "What happened?!", "fieldPostTitle": "Title", @@ -617,5 +618,6 @@ "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}", "postQuestionAnswered": "Answered Question", "postQuestionAnswerSelect": "Select as Answer", - "postQuestionAnswerSelected": "Answer has been selected, reward has been applied." + "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.", + "postVideoUpload": "Upload Video" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 1b0d446..3acb3df 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -139,6 +139,7 @@ "writePostTypeStory": "发动态", "writePostTypeArticle": "写文章", "writePostTypeQuestion": "提问题", + "writePostTypeVideo": "发视频", "fieldPostPublisher": "帖子发布者", "fieldPostContent": "发生什么事了?!", "fieldPostTitle": "标题", @@ -616,5 +617,6 @@ "postQuestionAnswered": "已解答的问题", "postQuestionAnswerTitle": "精选解答", "postQuestionAnswerSelect": "选择解答", - "postQuestionAnswerSelected": "解答已选择,奖励已发放。" + "postQuestionAnswerSelected": "解答已选择,奖励已发放。", + "postVideoUpload": "上传视频" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2845cc7..1fc959a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,7 +2,6 @@ PODS: - Alamofire (5.10.2) - connectivity_plus (0.0.1): - Flutter - - FlutterMacOS - croppy (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -180,7 +179,7 @@ PODS: - in_app_review (2.0.0): - Flutter - Kingfisher (8.2.0) - - livekit_client (2.3.5): + - livekit_client (2.3.6): - Flutter - flutter_webrtc - WebRTC-SDK (= 125.6422.06) @@ -237,7 +236,7 @@ PODS: DEPENDENCIES: - Alamofire - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - croppy (from `.symlinks/plugins/croppy/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -300,7 +299,7 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/darwin" + :path: ".symlinks/plugins/connectivity_plus/ios" croppy: :path: ".symlinks/plugins/croppy/ios" device_info_plus: @@ -374,7 +373,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 - connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321 device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -404,7 +403,7 @@ SPEC CHECKSUMS: image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d - livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976 + livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart index 4ed024c..84a3e8a 100644 --- a/lib/controllers/post_write_controller.dart +++ b/lib/controllers/post_write_controller.dart @@ -145,6 +145,7 @@ class PostWriteController extends ChangeNotifier { 'stories': 'writePostTypeStory', 'articles': 'writePostTypeArticle', 'questions': 'writePostTypeQuestion', + 'videos': 'writePostTypeVideo', }; static const kAttachmentProgressWeight = 0.9; @@ -197,6 +198,7 @@ class PostWriteController extends ChangeNotifier { PostWriteMedia? thumbnail; List attachments = List.empty(growable: true); DateTime? publishedAt, publishedUntil; + SnAttachment? videoAttachment; Future fetchRelatedPost( BuildContext context, { @@ -507,6 +509,7 @@ class PostWriteController extends ChangeNotifier { if (replyingPost != null) 'reply_to': replyingPost!.id, if (repostingPost != null) 'repost_to': repostingPost!.id, if (reward != null) 'reward': reward, + if (videoAttachment != null) 'video': videoAttachment!.rid, }, onSendProgress: (count, total) { progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2); @@ -633,6 +636,11 @@ class PostWriteController extends ChangeNotifier { notifyListeners(); } + void setVideoAttachment(SnAttachment? value) { + videoAttachment = value; + notifyListeners(); + } + void reset() { publishedAt = null; publishedUntil = null; diff --git a/lib/providers/post.dart b/lib/providers/post.dart index 0886fde..6e612e6 100644 --- a/lib/providers/post.dart +++ b/lib/providers/post.dart @@ -23,6 +23,9 @@ class SnPostContentProvider { if (out[i].body['thumbnail'] != null) { rids.add(out[i].body['thumbnail']); } + if (out[i].body['video'] != null) { + rids.add(out[i].body['video']); + } if (out[i].repostTo != null) { out[i] = out[i].copyWith( repostTo: await _preloadRelatedDataSingle(out[i].repostTo!), @@ -36,6 +39,7 @@ class SnPostContentProvider { preload: SnPostPreload( thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull, attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(), + video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull, ), ); } @@ -53,6 +57,9 @@ class SnPostContentProvider { if (out.body['thumbnail'] != null) { rids.add(out.body['thumbnail']); } + if (out.body['video'] != null) { + rids.add(out.body['video']); + } if (out.repostTo != null) { out = out.copyWith( repostTo: await _preloadRelatedDataSingle(out.repostTo!), @@ -64,6 +71,7 @@ class SnPostContentProvider { preload: SnPostPreload( thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull, attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(), + video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull, ), ); diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 7d4014d..edbc78f 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -1,4 +1,3 @@ -import 'package:animations/animations.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; @@ -7,10 +6,8 @@ import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:surface/providers/config.dart'; import 'package:surface/providers/post.dart'; import 'package:surface/providers/sn_network.dart'; -import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; @@ -97,8 +94,6 @@ class _ExploreScreenState extends State { @override Widget build(BuildContext context) { - final cfg = context.read(); - return AppScaffold( floatingActionButtonLocation: ExpandableFab.location, floatingActionButton: ExpandableFab( @@ -187,6 +182,27 @@ class _ExploreScreenState extends State { ), ], ), + Row( + children: [ + Text('writePostTypeVideo').tr(), + const Gap(20), + FloatingActionButton( + heroTag: null, + tooltip: 'writePostTypeVideo'.tr(), + onPressed: () { + GoRouter.of(context).pushNamed('postEditor', pathParameters: { + 'mode': 'videos', + }).then((value) { + if (value == true) { + _refreshPosts(); + } + }); + _fabKey.currentState!.toggle(); + }, + child: const Icon(Symbols.video_call), + ), + ], + ), ], ), body: RefreshIndicator( diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 97edcfa..ad81937 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; +import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; @@ -25,6 +26,9 @@ import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:provider/provider.dart'; +import 'package:uuid/uuid.dart'; + +import '../../widgets/attachment/attachment_input.dart'; class PostEditorExtra { final String? text; @@ -243,6 +247,10 @@ class _PostEditorScreenState extends State { controller: _writeController, onTapPublisher: _showPublisherPopup, ), + 'videos' => _PostVideoEditor( + controller: _writeController, + onTapPublisher: _showPublisherPopup, + ), _ => const Placeholder(), }, ), @@ -687,3 +695,115 @@ class _PostQuestionEditor extends StatelessWidget { ); } } + +class _PostVideoEditor extends StatelessWidget { + final PostWriteController controller; + final Function? onTapPublisher; + + const _PostVideoEditor({required this.controller, this.onTapPublisher}); + + void _selectVideo(BuildContext context) async { + final video = await showDialog( + context: context, + builder: (context) => AttachmentInputDialog( + title: 'postVideoUpload'.tr(), + pool: 'interactive', + mediaType: SnMediaType.video, + ), + ); + if (!context.mounted) return; + if (video == null) return; + controller.setVideoAttachment(video); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Material( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: InkWell( + child: Row( + children: [ + AccountImage(content: controller.publisher?.avatar, radius: 20), + const Gap(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(controller.publisher?.nick ?? 'loading'.tr()).bold(), + Text('@${controller.publisher?.name}'), + ], + ), + ), + ], + ).padding(horizontal: 12, vertical: 8), + onTap: () { + onTapPublisher?.call(); + }, + ), + ), + const Gap(16), + TextField( + controller: controller.titleController, + decoration: InputDecoration.collapsed( + hintText: 'fieldPostTitle'.tr(), + border: InputBorder.none, + ), + style: Theme.of(context).textTheme.titleLarge, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(horizontal: 16), + const Gap(8), + TextField( + controller: controller.descriptionController, + decoration: InputDecoration.collapsed( + hintText: 'fieldPostDescription'.tr(), + border: InputBorder.none, + ), + maxLines: null, + keyboardType: TextInputType.multiline, + style: Theme.of(context).textTheme.bodyLarge, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ).padding(horizontal: 16), + const Gap(12), + Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Theme.of(context).dividerColor), + ), + child: InkWell( + borderRadius: BorderRadius.circular(16), + child: AspectRatio( + aspectRatio: 16 / 9, + child: controller.videoAttachment == null + ? Center( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.add), + const Gap(4), + Text('postVideoUpload'.tr()), + ], + ), + ) + : ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AttachmentItem( + data: controller.videoAttachment!, + heroTag: const Uuid().v4(), + ), + ), + ), + onTap: () { + if (controller.videoAttachment != null) return; + _selectVideo(context); + }, + ), + ), + ], + ); + } +} diff --git a/lib/screens/post/post_search.dart b/lib/screens/post/post_search.dart index 7cf17e5..1daad54 100644 --- a/lib/screens/post/post_search.dart +++ b/lib/screens/post/post_search.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; diff --git a/lib/types/post.dart b/lib/types/post.dart index 66fa8b8..3912eb1 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -89,6 +89,7 @@ class SnPostPreload with _$SnPostPreload { const factory SnPostPreload({ required SnAttachment? thumbnail, required List? attachments, + required SnAttachment? video, }) = _SnPostPreload; factory SnPostPreload.fromJson(Map json) => diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index 8ee6a2a..89712ef 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -1567,6 +1567,7 @@ SnPostPreload _$SnPostPreloadFromJson(Map json) { mixin _$SnPostPreload { SnAttachment? get thumbnail => throw _privateConstructorUsedError; List? get attachments => throw _privateConstructorUsedError; + SnAttachment? get video => throw _privateConstructorUsedError; /// Serializes this SnPostPreload to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -1584,9 +1585,13 @@ abstract class $SnPostPreloadCopyWith<$Res> { SnPostPreload value, $Res Function(SnPostPreload) then) = _$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>; @useResult - $Res call({SnAttachment? thumbnail, List? attachments}); + $Res call( + {SnAttachment? thumbnail, + List? attachments, + SnAttachment? video}); $SnAttachmentCopyWith<$Res>? get thumbnail; + $SnAttachmentCopyWith<$Res>? get video; } /// @nodoc @@ -1606,6 +1611,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload> $Res call({ Object? thumbnail = freezed, Object? attachments = freezed, + Object? video = freezed, }) { return _then(_value.copyWith( thumbnail: freezed == thumbnail @@ -1616,6 +1622,10 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload> ? _value.attachments : attachments // ignore: cast_nullable_to_non_nullable as List?, + video: freezed == video + ? _value.video + : video // ignore: cast_nullable_to_non_nullable + as SnAttachment?, ) as $Val); } @@ -1632,6 +1642,20 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload> return _then(_value.copyWith(thumbnail: value) as $Val); }); } + + /// Create a copy of SnPostPreload + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SnAttachmentCopyWith<$Res>? get video { + if (_value.video == null) { + return null; + } + + return $SnAttachmentCopyWith<$Res>(_value.video!, (value) { + return _then(_value.copyWith(video: value) as $Val); + }); + } } /// @nodoc @@ -1642,10 +1666,15 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res> __$$SnPostPreloadImplCopyWithImpl<$Res>; @override @useResult - $Res call({SnAttachment? thumbnail, List? attachments}); + $Res call( + {SnAttachment? thumbnail, + List? attachments, + SnAttachment? video}); @override $SnAttachmentCopyWith<$Res>? get thumbnail; + @override + $SnAttachmentCopyWith<$Res>? get video; } /// @nodoc @@ -1663,6 +1692,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res> $Res call({ Object? thumbnail = freezed, Object? attachments = freezed, + Object? video = freezed, }) { return _then(_$SnPostPreloadImpl( thumbnail: freezed == thumbnail @@ -1673,6 +1703,10 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res> ? _value._attachments : attachments // ignore: cast_nullable_to_non_nullable as List?, + video: freezed == video + ? _value.video + : video // ignore: cast_nullable_to_non_nullable + as SnAttachment?, )); } } @@ -1682,7 +1716,8 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res> class _$SnPostPreloadImpl implements _SnPostPreload { const _$SnPostPreloadImpl( {required this.thumbnail, - required final List? attachments}) + required final List? attachments, + required this.video}) : _attachments = attachments; factory _$SnPostPreloadImpl.fromJson(Map json) => @@ -1700,9 +1735,12 @@ class _$SnPostPreloadImpl implements _SnPostPreload { return EqualUnmodifiableListView(value); } + @override + final SnAttachment? video; + @override String toString() { - return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments)'; + return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video)'; } @override @@ -1713,13 +1751,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload { (identical(other.thumbnail, thumbnail) || other.thumbnail == thumbnail) && const DeepCollectionEquality() - .equals(other._attachments, _attachments)); + .equals(other._attachments, _attachments) && + (identical(other.video, video) || other.video == video)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, thumbnail, - const DeepCollectionEquality().hash(_attachments)); + const DeepCollectionEquality().hash(_attachments), video); /// Create a copy of SnPostPreload /// with the given fields replaced by the non-null parameter values. @@ -1740,7 +1779,8 @@ class _$SnPostPreloadImpl implements _SnPostPreload { abstract class _SnPostPreload implements SnPostPreload { const factory _SnPostPreload( {required final SnAttachment? thumbnail, - required final List? attachments}) = _$SnPostPreloadImpl; + required final List? attachments, + required final SnAttachment? video}) = _$SnPostPreloadImpl; factory _SnPostPreload.fromJson(Map json) = _$SnPostPreloadImpl.fromJson; @@ -1749,6 +1789,8 @@ abstract class _SnPostPreload implements SnPostPreload { SnAttachment? get thumbnail; @override List? get attachments; + @override + SnAttachment? get video; /// Create a copy of SnPostPreload /// with the given fields replaced by the non-null parameter values. diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart index db63047..d2cb706 100644 --- a/lib/types/post.g.dart +++ b/lib/types/post.g.dart @@ -165,12 +165,16 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map json) => ? null : SnAttachment.fromJson(e as Map)) .toList(), + video: json['video'] == null + ? null + : SnAttachment.fromJson(json['video'] as Map), ); Map _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) => { 'thumbnail': instance.thumbnail?.toJson(), 'attachments': instance.attachments?.map((e) => e?.toJson()).toList(), + 'video': instance.video?.toJson(), }; _$SnBodyImpl _$$SnBodyImplFromJson(Map json) => _$SnBodyImpl( diff --git a/lib/widgets/attachment/attachment_input.dart b/lib/widgets/attachment/attachment_input.dart index a3b5bbe..7cbf7d8 100644 --- a/lib/widgets/attachment/attachment_input.dart +++ b/lib/widgets/attachment/attachment_input.dart @@ -6,12 +6,22 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_attachment.dart'; +import 'package:surface/types/attachment.dart'; import 'package:surface/widgets/dialog.dart'; class AttachmentInputDialog extends StatefulWidget { final String? title; -final bool? analyzeNow; - const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false}); + final bool? analyzeNow; + final SnMediaType? mediaType; + final String pool; + + const AttachmentInputDialog({ + super.key, + required this.title, + required this.pool, + this.analyzeNow = false, + this.mediaType = SnMediaType.image, + }); @override State createState() => _AttachmentInputDialogState(); @@ -20,13 +30,18 @@ final bool? analyzeNow; class _AttachmentInputDialogState extends State { final _randomIdController = TextEditingController(); - XFile? _thumbnailFile; + XFile? _file; + double? _progress; - void _pickImage() async { + void _pickMedia() async { final picker = ImagePicker(); - final result = await picker.pickImage(source: ImageSource.gallery); + final result = switch (widget.mediaType) { + SnMediaType.image => await picker.pickImage(source: ImageSource.gallery), + SnMediaType.video => await picker.pickVideo(source: ImageSource.gallery), + _ => await picker.pickMedia(), + }; if (result == null) return; - setState(() => _thumbnailFile = result); + setState(() => _file = result); } bool _isBusy = false; @@ -46,15 +61,20 @@ class _AttachmentInputDialogState extends State { if (!mounted) return; context.showErrorDialog(err); } - } else if (_thumbnailFile != null) { + } else if (_file != null) { try { - final attachment = await attach.directUploadOne( - (await _thumbnailFile!.readAsBytes()).buffer.asUint8List(), - _thumbnailFile!.path, - 'interactive', - null, + final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null); + + final attachment = await attach.chunkedUploadParts( + _file!, + place.$1, + place.$2, analyzeNow: widget.analyzeNow ?? false, + onProgress: (value) { + setState(() => _progress = value); + }, ); + if (!mounted) return; Navigator.pop(context, attachment); } catch (err) { @@ -67,7 +87,7 @@ class _AttachmentInputDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(widget.title ?? 'attachmentInputDialog').tr(), + title: Text(widget.title ?? 'attachmentInputDialog'.tr()), content: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -86,24 +106,35 @@ class _AttachmentInputDialogState extends State { const Gap(24), Text('attachmentInputNew').tr().fontSize(14), Card( - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - leading: const Icon(Symbols.add_photo_alternate), - trailing: const Icon(Symbols.chevron_right), - title: Text('addAttachmentFromAlbum').tr(), - subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(), - onTap: () { - _pickImage(); - }, + child: Column( + children: [ + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: const Icon(Symbols.add_photo_alternate), + trailing: const Icon(Symbols.chevron_right), + title: Text('addAttachmentFromAlbum').tr(), + subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(), + onTap: () { + _pickMedia(); + }, + ), + ], ), ), + if (_isBusy) + LinearProgressIndicator( + value: _progress, + borderRadius: BorderRadius.all(Radius.circular(8)), + ).padding(top: 16), ], ), actions: [ TextButton( - onPressed: _isBusy ? null : () { - Navigator.pop(context); - }, + onPressed: _isBusy + ? null + : () { + Navigator.pop(context); + }, child: Text('dialogDismiss').tr(), ), TextButton( diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 98f8ea5..e285453 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -28,6 +28,7 @@ import 'package:surface/types/attachment.dart'; import 'package:surface/types/post.dart'; import 'package:surface/types/reaction.dart'; import 'package:surface/widgets/account/account_image.dart'; +import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/link_preview.dart'; @@ -210,6 +211,7 @@ class PostItem extends StatelessWidget { if (onDeleted != null) {} }, ).padding(horizontal: 12, top: 8, bottom: 8), + if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12), @@ -293,6 +295,7 @@ class PostItem extends StatelessWidget { if (onDeleted != null) onDeleted!(); }, ).padding(horizontal: 12, vertical: 8), + if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), if (data.body['title'] != null || data.body['description'] != null) _PostHeadline( @@ -1520,3 +1523,29 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { ); } } + +class _PostVideoPlayer extends StatelessWidget { + final SnPost data; + + const _PostVideoPlayer({required this.data}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'), + ), + ), + ); + } +} diff --git a/lib/widgets/post/post_media_pending_list.dart b/lib/widgets/post/post_media_pending_list.dart index 318e7c5..a60c039 100644 --- a/lib/widgets/post/post_media_pending_list.dart +++ b/lib/widgets/post/post_media_pending_list.dart @@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget { context: context, builder: (context) => AttachmentInputDialog( title: 'attachmentSetThumbnail'.tr(), + pool: 'interactive', analyzeNow: true, ), ); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 69bbdbf..3436908 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,7 +2,6 @@ PODS: - bitsdojo_window_macos (0.0.1): - FlutterMacOS - connectivity_plus (0.0.1): - - Flutter - FlutterMacOS - croppy (0.0.1): - FlutterMacOS @@ -143,7 +142,7 @@ PODS: - HotKey - in_app_review (2.0.0): - FlutterMacOS - - livekit_client (2.3.5): + - livekit_client (2.3.6): - flutter_webrtc - FlutterMacOS - WebRTC-SDK (= 125.6422.06) @@ -190,7 +189,7 @@ PODS: DEPENDENCIES: - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) @@ -244,7 +243,7 @@ EXTERNAL SOURCES: bitsdojo_window_macos: :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos connectivity_plus: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos croppy: :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos device_info_plus: @@ -308,7 +307,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 + connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802 croppy: 25a638bd7d05411d8c697f481568f261037694fc device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af @@ -334,7 +333,7 @@ SPEC CHECKSUMS: HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160 in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 - livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1 + livekit_client: 0ad107154753a5a76802d2222c040223ad049499 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 diff --git a/pubspec.lock b/pubspec.lock index de2fcd0..cf2b03d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -214,6 +214,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.3" + chalkdart: + dependency: transitive + description: + name: chalkdart + sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40" + url: "https://pub.dev" + source: hosted + version: "2.3.2" characters: dependency: transitive description: @@ -266,10 +274,10 @@ packages: dependency: transitive description: name: connectivity_plus - sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412" + sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.3" connectivity_plus_platform_interface: dependency: transitive description: @@ -362,10 +370,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a + sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da" url: "https://pub.dev" source: hosted - version: "11.2.2" + version: "11.3.0" device_info_plus_platform_interface: dependency: transitive description: @@ -490,10 +498,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc + sha256: cacfdc5abe93e64d418caa9256eef663499ad791bb688d9fd12c85a311968fba url: "https://pub.dev" source: hosted - version: "8.3.1" + version: "8.3.2" file_saver: dependency: "direct main" description: @@ -830,10 +838,10 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03" + sha256: e917067abeef2400e6a7a03db53a6e1418551e54809f18ab80447ac323eb77e4 url: "https://pub.dev" source: hosted - version: "0.12.7" + version: "0.12.8" freezed: dependency: "direct dev" description: @@ -886,10 +894,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" + sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651" url: "https://pub.dev" source: hosted - version: "14.7.2" + version: "14.8.0" google_fonts: dependency: "direct main" description: @@ -934,10 +942,10 @@ packages: dependency: "direct main" description: name: home_widget - sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12 + sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.0+1" hotkey_manager: dependency: "direct main" description: @@ -1190,10 +1198,10 @@ packages: dependency: "direct main" description: name: livekit_client - sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de" + sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" logging: dependency: transitive description: @@ -1246,10 +1254,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd" + sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14" url: "https://pub.dev" source: hosted - version: "4.2801.1" + version: "4.2805.1" media_kit: dependency: "direct main" description: @@ -1382,10 +1390,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254 + sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35" url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.2.1" package_info_plus_platform_interface: dependency: transitive description: @@ -2251,10 +2259,10 @@ packages: dependency: transitive description: name: webrtc_interface - sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388 + sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" win32: dependency: transitive description: