flutter_video_editor_plugin 1.0.8
flutter_video_editor_plugin: ^1.0.8 copied to clipboard
Timeline-based multi-clip video editor: trim, split, stickers, text, color filters, transitions and audio. Preview and export share one native composition.
example/lib/main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_video_editor_plugin/flutter_video_editor_plugin.dart';
import 'package:gal/gal.dart';
import 'package:image_picker/image_picker.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(theme: ThemeData.dark(), home: const HomePage());
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
enum _SaveState { idle, saving, saved, failed }
class _HomePageState extends State<HomePage> {
VideoEditorResult? _lastResult;
_SaveState _saved = _SaveState.idle;
String? _saveError;
bool _draftsEnabled = false;
bool _coverPickerEnabled = true;
List<VideoEditorDraft> _drafts = const [];
// Demo keys. Do not ship these.
static const _giphyApiKey = 'RLm06zs0imxFCeWYaRLILoFEoMNUSquN';
static const _licenseKey = 've_live_ge7y5gywjua57qj99sk6gaugs87c599g';
VideoEditorConfig get _config => VideoEditorConfig(
licenseKey: _licenseKey,
giphyApiKey: _giphyApiKey,
minDurationSeconds: 3,
maxDurationSeconds: 120,
draftsEnabled: _draftsEnabled,
coverPickerEnabled: _coverPickerEnabled,
);
@override
void initState() {
super.initState();
_reloadDrafts();
}
Future<void> _reloadDrafts() async {
final drafts = await VideoEditor.drafts();
if (mounted) setState(() => _drafts = drafts);
}
Future<void> _openEditor({String? initialSource, String? draftId}) async {
final result = await VideoEditor.present(
initialSource: initialSource,
draftId: draftId,
config: _config,
onFailure: (failure) => debugPrint('[example] editor failed: $failure'),
);
if (!mounted) return;
await _reloadDrafts();
if (result == null || !mounted) return;
setState(() {
_lastResult = result;
_saved = _SaveState.saving;
_saveError = null;
});
await _saveToGallery(result);
}
Future<void> _saveToGallery(VideoEditorResult result) async {
try {
if (!await Gal.hasAccess() && !await Gal.requestAccess()) {
_saveFailed('access denied');
return;
}
await Gal.putVideo(result.videoPath);
if (mounted) setState(() => _saved = _SaveState.saved);
} on GalException catch (e) {
debugPrint('[example] save failed: $e');
_saveFailed(e.type.name);
}
}
void _saveFailed(String why) {
if (!mounted) return;
setState(() {
_saved = _SaveState.failed;
_saveError = why;
});
}
Future<void> _openEditorWithPicked() async {
final picked = await ImagePicker().pickVideo(source: ImageSource.gallery);
if (picked == null || !mounted) return;
await _openEditor(initialSource: picked.path);
}
Future<void> _openDraft(VideoEditorDraft draft) async {
setState(() => _draftsEnabled = true);
await _openEditor(draftId: draft.id);
}
Future<void> _deleteDraft(VideoEditorDraft draft) async {
await VideoEditor.deleteDraft(draft.id);
await _reloadDrafts();
}
String get _saveLabel => switch (_saved) {
_SaveState.idle => '',
_SaveState.saving => 'Saving to gallery…',
_SaveState.saved => 'Saved to gallery',
_SaveState.failed => 'Could not save to gallery${_saveError == null ? '' : ' — $_saveError'}',
};
@override
Widget build(BuildContext context) {
final result = _lastResult;
return Scaffold(
appBar: AppBar(title: const Text('Video Editor Demo')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 24),
children: [
Center(
child: Column(
children: [
ElevatedButton(
onPressed: _openEditor,
child: const Text('Open Video Editor'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: _openEditorWithPicked,
child: const Text('Pick first, then edit'),
),
],
),
),
SwitchListTile(
title: const Text('Drafts'),
subtitle: const Text('Leaving the editor saves a draft'),
value: _draftsEnabled,
onChanged: (value) => setState(() => _draftsEnabled = value),
),
SwitchListTile(
title: const Text('Cover picker'),
subtitle: const Text('Choose a cover frame or photo before rendering'),
value: _coverPickerEnabled,
onChanged: (value) => setState(() => _coverPickerEnabled = value),
),
if (result != null) _ResultSection(result: result, saveLabel: _saveLabel, saved: _saved),
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
'Drafts (${_drafts.length})',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
),
if (_drafts.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('No drafts', style: TextStyle(fontSize: 12)),
),
for (final draft in _drafts)
_DraftTile(
draft: draft,
onOpen: () => _openDraft(draft),
onDelete: () => _deleteDraft(draft),
),
],
),
);
}
}
class _DraftTile extends StatelessWidget {
const _DraftTile({required this.draft, required this.onOpen, required this.onDelete});
final VideoEditorDraft draft;
final VoidCallback onOpen;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
final hasCover = draft.coverPath.isNotEmpty && File(draft.coverPath).existsSync();
return ListTile(
onTap: onOpen,
leading: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: SizedBox(
width: 36,
height: 64,
child: hasCover
? Image.file(File(draft.coverPath), fit: BoxFit.cover)
: const ColoredBox(color: Colors.white12),
),
),
title: Text(
'${draft.clipCount} clip${draft.clipCount == 1 ? '' : 's'} · '
'${draft.durationSec.toStringAsFixed(1)}s',
),
subtitle: Text(
'Updated ${draft.updatedAt.toLocal().toString().substring(0, 16)}',
style: const TextStyle(fontSize: 12),
),
trailing: IconButton(icon: const Icon(Icons.delete_outline), onPressed: onDelete),
);
}
}
class _ResultSection extends StatelessWidget {
const _ResultSection({required this.result, required this.saveLabel, required this.saved});
final VideoEditorResult result;
final String saveLabel;
final _SaveState saved;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Last Result:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
if (result.coverPath.isNotEmpty && File(result.coverPath).existsSync())
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(result.coverPath),
width: 120,
height: 200,
fit: BoxFit.cover,
),
),
const SizedBox(height: 8),
Text('Video: ${result.videoPath}', style: const TextStyle(fontSize: 12)),
const SizedBox(height: 4),
Text('Cover: ${result.coverPath}', style: const TextStyle(fontSize: 12)),
const SizedBox(height: 4),
Text(
'Duration: ${result.durationSec.toStringAsFixed(1)}s',
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 4),
Text('Audio: ${result.audioPaths.isEmpty ? '-' : result.audioPaths.join(', ')}', style: const TextStyle(fontSize: 12)),
const SizedBox(height: 8),
Text(
saveLabel,
style: TextStyle(
fontSize: 12,
color: saved == _SaveState.failed ? Colors.redAccent : null,
),
),
],
),
);
}
}