✨ Live video
This commit is contained in:
@ -219,6 +219,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
SnAttachment? videoAttachment;
|
||||
String videoUrl = '';
|
||||
bool videoLive = false;
|
||||
SnPoll? poll;
|
||||
|
||||
Future<void> fetchRelatedPost(
|
||||
@ -445,8 +447,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
rewardController.text = data['reward']?.toString() ?? '';
|
||||
if (data['thumbnail'] != null)
|
||||
if (data['thumbnail'] != null) {
|
||||
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
}
|
||||
attachments.addAll(data['attachments']
|
||||
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
|
||||
.cast<PostWriteMedia>());
|
||||
@ -455,10 +458,12 @@ class PostWriteController extends ChangeNotifier {
|
||||
visibility = data['visibility'];
|
||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||
if (data['published_at'] != null)
|
||||
if (data['published_at'] != null) {
|
||||
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null)
|
||||
}
|
||||
if (data['published_until'] != null) {
|
||||
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
}
|
||||
replyingPost =
|
||||
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost =
|
||||
@ -595,7 +600,8 @@ 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,
|
||||
if (videoAttachment != null || videoUrl.isNotEmpty)
|
||||
'video': videoUrl.isNotEmpty ? videoUrl : videoAttachment!.rid,
|
||||
if (poll != null) 'poll': poll!.id,
|
||||
if (realm != null) 'realm': realm!.id,
|
||||
'is_draft': saveAsDraft,
|
||||
@ -738,6 +744,15 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVideoUrl(String value) {
|
||||
videoUrl = value;
|
||||
}
|
||||
|
||||
void setVideoLive(bool value) {
|
||||
videoLive = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPoll(SnPoll? value) {
|
||||
poll = value;
|
||||
notifyListeners();
|
||||
|
@ -1105,7 +1105,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoEditor extends StatelessWidget {
|
||||
class _PostVideoEditor extends StatefulWidget {
|
||||
final PostWriteController controller;
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
@ -1113,7 +1113,16 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
const _PostVideoEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
void _selectVideo(BuildContext context) async {
|
||||
@override
|
||||
State<_PostVideoEditor> createState() => _PostVideoEditorState();
|
||||
}
|
||||
|
||||
class _PostVideoEditorState extends State<_PostVideoEditor> {
|
||||
String? _renderer;
|
||||
|
||||
final TextEditingController _streamUrlController = TextEditingController();
|
||||
|
||||
void _selectVideo() async {
|
||||
final video = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
@ -1124,7 +1133,25 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (video == null) return;
|
||||
controller.setVideoAttachment(video);
|
||||
widget.controller.setVideoAttachment(video);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_streamUrlController.addListener(() {
|
||||
if (_streamUrlController.text.isEmpty) {
|
||||
widget.controller.setVideoUrl('');
|
||||
} else {
|
||||
widget.controller.setVideoUrl(_streamUrlController.text);
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamUrlController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1142,10 +1169,10 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapPublisher?.call();
|
||||
widget.onTapPublisher?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.publisher?.avatar,
|
||||
content: widget.controller.publisher?.avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1155,10 +1182,10 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTapRealm?.call();
|
||||
widget.onTapRealm?.call();
|
||||
},
|
||||
child: AccountImage(
|
||||
content: controller.realm?.avatar,
|
||||
content: widget.controller.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
||||
radius: 14,
|
||||
),
|
||||
@ -1171,7 +1198,7 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
children: [
|
||||
const Gap(6),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
controller: widget.controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
@ -1182,7 +1209,7 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
controller: widget.controller.descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
@ -1194,57 +1221,89 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
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),
|
||||
onTap: controller.videoAttachment == null
|
||||
? () => _selectVideo(context)
|
||||
: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PendingAttachmentActionSheet(
|
||||
media: PostWriteMedia(
|
||||
controller.videoAttachment!,
|
||||
if (widget.controller.videoLive)
|
||||
TextField(
|
||||
controller: _streamUrlController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostVideoStreamUrl'.tr(),
|
||||
helperText: 'fieldPostVideoStreamUrlDescription'.tr(),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 12)
|
||||
else
|
||||
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),
|
||||
onTap: widget.controller.videoAttachment == null
|
||||
? () => _selectVideo()
|
||||
: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PendingAttachmentActionSheet(
|
||||
media: PostWriteMedia(
|
||||
widget.controller.videoAttachment!,
|
||||
),
|
||||
),
|
||||
).then((value) async {
|
||||
if (value is PostWriteMedia) {
|
||||
widget.controller
|
||||
.setVideoAttachment(value.attachment);
|
||||
} else if (value == false) {
|
||||
widget.controller.setVideoAttachment(null);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: widget.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: widget.controller.videoAttachment!,
|
||||
heroTag: const Uuid().v4(),
|
||||
),
|
||||
),
|
||||
).then((value) async {
|
||||
if (value is PostWriteMedia) {
|
||||
controller.setVideoAttachment(value.attachment);
|
||||
} else if (value == false) {
|
||||
controller.setVideoAttachment(null);
|
||||
}
|
||||
});
|
||||
},
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.live_tv),
|
||||
title: Text('postVideoLive').tr(),
|
||||
subtitle: Text('postVideoLiveDescription').tr(),
|
||||
value: widget.controller.videoLive,
|
||||
onChanged: (value) =>
|
||||
widget.controller.setVideoLive(value ?? false),
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.web),
|
||||
title: Text('postVideoRendererWeb').tr(),
|
||||
subtitle: Text('postVideoRendererWebDescription').tr(),
|
||||
value: _renderer == 'web',
|
||||
onChanged: (value) => setState(
|
||||
() => _renderer = (value ?? false) ? 'web' : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@ -41,6 +42,7 @@ import 'package:surface/widgets/post/post_poll.dart';
|
||||
import 'package:surface/widgets/post/post_reaction.dart';
|
||||
import 'package:surface/widgets/post/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class OpenablePostItem extends StatelessWidget {
|
||||
@ -2321,24 +2323,48 @@ class _PostVideoPlayer extends StatelessWidget {
|
||||
|
||||
@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.body['video'],
|
||||
heroTag: 'post-video-${data.id}',
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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: (data.body['video'] is String)
|
||||
? InAppWebView(
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri(data.body['video']),
|
||||
),
|
||||
)
|
||||
: AttachmentItem(
|
||||
data: data.body['video'],
|
||||
heroTag: 'post-video-${data.id}',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (data.body['video'] is String)
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.launch, size: 16),
|
||||
const Gap(6),
|
||||
Text('openInBrowser').tr(),
|
||||
],
|
||||
).opacity(0.8),
|
||||
onTap: () {
|
||||
launchUrlString(data.body['video']);
|
||||
},
|
||||
).padding(top: 4),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user