✨ Compress video
This commit is contained in:
@ -215,7 +215,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (widget.data.thumbnail != null)
|
||||
if (widget.data.thumbnail?.isNotEmpty ?? false)
|
||||
AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(widget.data.thumbnail!),
|
||||
fit: BoxFit.cover,
|
||||
|
163
lib/widgets/attachment/pending_attachment_compress.dart
Normal file
163
lib/widgets/attachment/pending_attachment_compress.dart
Normal file
@ -0,0 +1,163 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
class PendingVideoCompressDialog extends StatefulWidget {
|
||||
final PostWriteMedia media;
|
||||
|
||||
const PendingVideoCompressDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingVideoCompressDialog> createState() => _PendingVideoCompressDialogState();
|
||||
}
|
||||
|
||||
class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog> {
|
||||
VideoQuality _quality = VideoQuality.DefaultQuality;
|
||||
|
||||
bool _isBusy = false;
|
||||
double? _progress;
|
||||
MediaInfo? _mediaInfo;
|
||||
|
||||
Subscription? _progressSubscription;
|
||||
|
||||
Future<void> _startCompress() async {
|
||||
_mediaInfo = await VideoCompress.compressVideo(
|
||||
widget.media.file!.path,
|
||||
quality: _quality,
|
||||
deleteOrigin: false,
|
||||
frameRate: switch (_quality) {
|
||||
VideoQuality.HighestQuality => 60,
|
||||
VideoQuality.DefaultQuality => 60,
|
||||
_ => 30,
|
||||
},
|
||||
);
|
||||
if (_mediaInfo == null) return;
|
||||
setState(() => _isBusy = true);
|
||||
if (!mounted || _mediaInfo == null) return;
|
||||
Navigator.pop(context, PostWriteMedia.fromFile(XFile(_mediaInfo!.path!)));
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_progressSubscription = VideoCompress.compressProgress$.subscribe((event) {
|
||||
log('[Compress] Progress: $event');
|
||||
setState(() {
|
||||
_progress = event / 100;
|
||||
_isBusy = event < 100;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_progressSubscription?.unsubscribe();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('attachmentCompressVideo').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: widget.media.file?.length(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||
return Text(
|
||||
snapshot.data!.formatBytes(),
|
||||
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('attachmentCompressQuality').tr(),
|
||||
const Gap(8),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile(
|
||||
title: Text('attachmentCompressQualityHighest').tr(),
|
||||
value: VideoQuality.HighestQuality,
|
||||
groupValue: _quality,
|
||||
selected: _quality == VideoQuality.HighestQuality,
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
setState(() => _quality = val);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: Text('attachmentCompressQualityDefault').tr(),
|
||||
value: VideoQuality.DefaultQuality,
|
||||
groupValue: _quality,
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
setState(() => _quality = val);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: Text('attachmentCompressQualityMedium').tr(),
|
||||
value: VideoQuality.MediumQuality,
|
||||
groupValue: _quality,
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
setState(() => _quality = val);
|
||||
}
|
||||
},
|
||||
),
|
||||
RadioListTile(
|
||||
title: Text('attachmentCompressQualityLow').tr(),
|
||||
value: VideoQuality.LowQuality,
|
||||
groupValue: _quality,
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
setState(() => _quality = val);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text('attachmentCompressQualityHint', style: Theme.of(context).textTheme.bodySmall!).tr(),
|
||||
if (_isBusy)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _progress ?? 0),
|
||||
duration: Duration(milliseconds: 100),
|
||||
builder: (context, value, _) => LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
).padding(top: 16),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : _startCompress,
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -24,6 +24,8 @@ import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
import '../attachment/pending_attachment_compress.dart';
|
||||
|
||||
class PostMediaPendingList extends StatelessWidget {
|
||||
final PostWriteMedia? thumbnail;
|
||||
final List<PostWriteMedia> attachments;
|
||||
@ -118,9 +120,28 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _compressVideo(BuildContext context, int idx) async {
|
||||
final result = await showDialog<PostWriteMedia?>(
|
||||
context: context,
|
||||
builder: (context) => PendingVideoCompressDialog(media: attachments[idx]),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
onUpdate!(idx, result);
|
||||
}
|
||||
|
||||
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
||||
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||
return ContextMenu(
|
||||
entries: [
|
||||
if (media.attachment == null && media.type == SnMediaType.video && canCompressVideo)
|
||||
MenuItem(
|
||||
label: 'attachmentCompressVideo'.tr(),
|
||||
icon: Symbols.compress,
|
||||
onSelected: () {
|
||||
_compressVideo(context, idx);
|
||||
},
|
||||
),
|
||||
if (media.attachment != null && media.type == SnMediaType.video)
|
||||
MenuItem(
|
||||
label: 'attachmentSetThumbnail'.tr(),
|
||||
@ -306,22 +327,22 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
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!)),
|
||||
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
]),
|
||||
],
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!)),
|
||||
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(),
|
||||
|
Reference in New Issue
Block a user