✨ Video post
This commit is contained in:
@ -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<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
SnAttachment? videoAttachment;
|
||||
|
||||
Future<void> 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;
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -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<ExploreScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
@ -187,6 +182,27 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
|
@ -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<PostEditorScreen> {
|
||||
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<SnAttachment?>(
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -89,6 +89,7 @@ class SnPostPreload with _$SnPostPreload {
|
||||
const factory SnPostPreload({
|
||||
required SnAttachment? thumbnail,
|
||||
required List<SnAttachment?>? attachments,
|
||||
required SnAttachment? video,
|
||||
}) = _SnPostPreload;
|
||||
|
||||
factory SnPostPreload.fromJson(Map<String, Object?> json) =>
|
||||
|
@ -1567,6 +1567,7 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
||||
mixin _$SnPostPreload {
|
||||
SnAttachment? get thumbnail => throw _privateConstructorUsedError;
|
||||
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
|
||||
SnAttachment? get video => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPostPreload to a JSON map.
|
||||
Map<String, dynamic> 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<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? 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<SnAttachment?>?,
|
||||
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<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? 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<SnAttachment?>?,
|
||||
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<SnAttachment?>? attachments})
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required this.video})
|
||||
: _attachments = attachments;
|
||||
|
||||
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> 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<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required final SnAttachment? video}) = _$SnPostPreloadImpl;
|
||||
|
||||
factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPostPreloadImpl.fromJson;
|
||||
@ -1749,6 +1789,8 @@ abstract class _SnPostPreload implements SnPostPreload {
|
||||
SnAttachment? get thumbnail;
|
||||
@override
|
||||
List<SnAttachment?>? get attachments;
|
||||
@override
|
||||
SnAttachment? get video;
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
@ -165,12 +165,16 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||
? null
|
||||
: SnAttachment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
video: json['video'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['video'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||
'video': instance.video?.toJson(),
|
||||
};
|
||||
|
||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
||||
|
@ -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<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||
@ -20,13 +30,18 @@ final bool? analyzeNow;
|
||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
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<AttachmentInputDialog> {
|
||||
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<AttachmentInputDialog> {
|
||||
@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<AttachmentInputDialog> {
|
||||
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(
|
||||
|
@ -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}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
pool: 'interactive',
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
|
Reference in New Issue
Block a user