diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4edadd9f..10d547f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,108 +1,120 @@
+## 2.0.0
+
+* **BREAKING CHANGE:** Overhaul of the entire video trimmer implementation.
+ * Two types of `TrimViewer` are available: `FixedTrimViewer` & `ScrollableTrimViewer`. By default it's set to `auto` so that it switches between these two based on the total video length and the maximum trim duration allowed.
+ * Rename `TrimEditor` to `TrimViewer`. Check out [this diagram](https://raw.githubusercontent.com/sbis04/video_trimmer/new_editor/screenshots/trim_viewer_preview_small.png) for better understanding of the keywords/terms.
+ * Separate the properties of into two types, `TrimEditorProperties` & `TrimAreaProperties`. Helps in identifying where the properties belong to easily.
+ * Fix some bugs related to wrong trimmer length while initialization.
+* Add more customization options.
+* Improve the documentation.
+* Update the example project.
+* Bug Fix: Use `circleSizeOnDrag` only on the selected holder.
+
## 1.2.0
-* Update plugin versions
+* Update plugin versions.
## 1.1.3
-* Changing `path` version to `1.8.0` (latest is `1.8.1`) because it creates a conflict with `flutter_test` as it uses the older version
+* Changing `path` version to `1.8.0` (latest is `1.8.1`) because it creates a conflict with `flutter_test` as it uses the older version.
## 1.1.2
-* Update plugin versions
+* Update plugin versions.
## 1.1.1
-* Format file (to pass static analysis)
+* Format file (to pass static analysis).
## 1.1.0
-* Update ffmpeg_kit_flutter to 4.5.1-LTS
-* Update other plugin versions
-* Update Readme
+* Update ffmpeg_kit_flutter to 4.5.1-LTS.
+* Update other plugin versions.
+* Update Readme.
## 1.0.0
* **BREAKING CHANGE:** Migrate to [FFmpegKit for Flutter](https://pub.dev/packages/ffmpeg_kit_flutter).
`saveTrimmedVideo()` method is not async now, you'll need to use the callback `onSave: (outputPath) {}` to get the trimmed video output path.
-* Add playback timestamp in the `showDuration`
-* Simply configuration
-* Update the plugin versions
-* Update Docs
+* Add playback timestamp in the `showDuration`.
+* Simply configuration.
+* Update the plugin versions.
+* Update Docs.
## 0.6.0
-* Update the plugin versions
-* Using flutter_lints
+* Update the plugin versions.
+* Using flutter_lints.
## 0.5.4
-* Update Docs
+* Update Docs.
## 0.5.3
-* Add `borderWidth` and `scrubberWidth` properties under `TrimEditor` widget
-* Fix padding and border decoration of `VideoViewer`
-* Update the plugin versions
+* Add `borderWidth` and `scrubberWidth` properties under `TrimEditor` widget.
+* Fix padding and border decoration of `VideoViewer`.
+* Update the plugin versions.
## 0.5.2
-* Fix iOS error while loading thumbnails in `TrimEditor`
-* Remove an unused dependency
-* Update packages
+* Fix iOS error while loading thumbnails in `TrimEditor`.
+* Remove an unused dependency.
+* Update packages.
## 0.5.1
-* Update the example in Readme
-* Update the plugin versions
+* Update the example in Readme.
+* Update the plugin versions.
## 0.5.0
-* Global refactoring, example is now a standalone screen
-* Fixed the staggering issue when dragging the frame
-* The whole frame can now be dragged in addition to the sides
-* Updated packages
-* Updated the example
+* Global refactoring, example is now a standalone screen.
+* Fixed the staggering issue when dragging the frame.
+* The whole frame can now be dragged in addition to the sides.
+* Updated packages.
+* Updated the example.
## 0.4.0
-* Migrate to null safety
-* Fix video thumbnail loading issues
-* Bump up all dependencies
-* Upgrade example
+* Migrate to null safety.
+* Fix video thumbnail loading issues.
+* Bump up all dependencies.
+* Upgrade example.
## 0.3.5
-* Update example app (small bug fixes)
-* Update to latest plugin versions
+* Update example app (small bug fixes).
+* Update to latest plugin versions.
## 0.3.4
-* Fixed the issue with video getting struck for a few initial frames during playback
+* Fixed the issue with video getting struck for a few initial frames during playback.
## 0.3.3
-* Updated plugin versions
+* Updated plugin versions.
## 0.3.2
-* Minor changes
+* Minor changes.
## 0.3.1
-* Improve the file structure of the package
-* Now, you just have to import one file for using the package
+* Improve the file structure of the package.
+* Now, you just have to import one file for using the package.
## 0.3.0
-* Update the plugin versions
-* Update example app (now includes how to retrieve the trimmed video)
-* Update Readme
-* Fixes some memory leak issues
+* Update the plugin versions.
+* Update example app (now includes how to retrieve the trimmed video).
+* Update Readme.
+* Fixes some memory leak issues.
## 0.2.7
* Add a new property called `maxVideoLength` for specifying the max length of the output video.
-* Update Docs
+* Update Docs.
## 0.2.6
@@ -111,62 +123,62 @@
**NOTE:** Applying this will take significantly greater amount of time to process the output video.
-* Improve Docs
+* Improve Docs.
## 0.2.5
-* Update Docs
-* Reverted the FFmpeg trimmed video start & end position to **milliseconds** (earlier it was changed to **seconds** in `v0.2.4` to fix video freezing, but after testing it was found that the issue still persists)
+* Update Docs.
+* Reverted the FFmpeg trimmed video start & end position to **milliseconds** (earlier it was changed to **seconds** in `v0.2.4` to fix video freezing, but after testing it was found that the issue still persists).
## 0.2.4
-* Fix output video freezing during start and end
-* Update the example app to use LTS version of FFmpeg (for wider device support)
-* Update Readme
+* Fix output video freezing during start and end.
+* Update the example app to use LTS version of FFmpeg (for wider device support).
+* Update Readme.
## 0.2.3
-* Fix issue with path returned
+* Fix issue with path returned.
## 0.2.2
-* Change implementation of the `saveTrimmedVideo()` method
-* `saveTrimmedVideo()` now returns the output video path
-* Update Docs
+* Change implementation of the `saveTrimmedVideo()` method.
+* `saveTrimmedVideo()` now returns the output video path.
+* Update Docs.
## 0.2.1
-* Fix over-scrolling && scroll-over issue
+* Fix over-scrolling && scroll-over issue.
## 0.2.0
* BREAKING CHANGE: `loadVideo()` method implementation changed.
Now, you can pass the video file to the method.
-* Fix issue related to animation controller improperly disposing
-* Update Docs
+* Fix issue related to animation controller improperly disposing.
+* Update Docs.
## 0.1.5
-* Fix for paths having white spaces
+* Fix for paths having white spaces.
## 0.1.4
-* Smoothen the scrubber animation
+* Smoothen the scrubber animation.
## 0.1.3
-* Code improvements
-* Update Readme
+* Code improvements.
+* Update Readme.
## 0.1.2
-* Changed `StorageDir` format naming
-* Update documentation
+* Changed `StorageDir` format naming.
+* Update documentation.
## 0.1.1
-* Correct documentation
+* Correct documentation.
## 0.1.0
-* Initial Open Source release
+* Initial Open Source release.
diff --git a/README.md b/README.md
index 8dc79ed8..ba9152ea 100644
--- a/README.md
+++ b/README.md
@@ -19,13 +19,22 @@
### Features
-* Customizable video trimmer
-* Video playback control
-* Retrieving and storing video file
+* Customizable video trimmer.
+* Supports two types of trim viewer, fixed length and scrollable.
+* Video playback control.
+* Retrieving and storing video file.
Also, supports conversion to **GIF**.
-
TRIM EDITOR
+> **Migrating to v2.0.0:** If you were using 1.x.x version of this package, checkout the BREAKING CHANGES by going to the **Changelog** tab on the `pub.dev` package page.
+
+Following image shows the structure of the `TrimViewer`. It consists of the `Duration` on top (displaying the start, end, and scrubber time), `TrimArea` consisting of the thumbnails, and `TrimEditor` which is an overlay that let's you select a portion from the video.
+
+
+
+
+
+TRIM VIEWER
@@ -37,7 +46,7 @@ Also, supports conversion to **GIF**.
-CUSTOMIZABLE VIDEO EDITOR
+CUSTOMIZABLE VIDEO TRIMMER
@@ -49,7 +58,7 @@ Add the dependency `video_trimmer` to your **pubspec.yaml** file:
```yaml
dependencies:
- video_trimmer: ^1.1.0
+ video_trimmer: ^2.0.0
```
### Android configuration
@@ -184,22 +193,15 @@ VideoViewer(trimmer: _trimmer)
### Display the video trimmer area
```dart
-TrimEditor(
+TrimViewer(
trimmer: _trimmer,
viewerHeight: 50.0,
viewerWidth: MediaQuery.of(context).size.width,
- maxVideoLength: Duration(seconds: 10),
- onChangeStart: (value) {
- _startValue = value;
- },
- onChangeEnd: (value) {
- _endValue = value;
- },
- onChangePlaybackState: (value) {
- setState(() {
- _isPlaying = value;
- });
- },
+ maxVideoLength: const Duration(seconds: 10),
+ onChangeStart: (value) => _startValue = value,
+ onChangeEnd: (value) => _endValue = value,
+ onChangePlaybackState: (value) =>
+ setState(() => _isPlaying = value),
)
```
@@ -351,22 +353,15 @@ class _TrimmerViewState extends State {
child: VideoViewer(trimmer: _trimmer),
),
Center(
- child: TrimEditor(
+ child: TrimViewer(
trimmer: _trimmer,
viewerHeight: 50.0,
viewerWidth: MediaQuery.of(context).size.width,
- maxVideoLength: Duration(seconds: 10),
- onChangeStart: (value) {
- _startValue = value;
- },
- onChangeEnd: (value) {
- _endValue = value;
- },
- onChangePlaybackState: (value) {
- setState(() {
- _isPlaying = value;
- });
- },
+ maxVideoLength: const Duration(seconds: 10),
+ onChangeStart: (value) => _startValue = value,
+ onChangeEnd: (value) => _endValue = value,
+ onChangePlaybackState: (value) =>
+ setState(() => _isPlaying = value),
),
),
TextButton(
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index 35cf6340..680955fe 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -407,7 +407,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 15.5;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
@@ -545,7 +545,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 15.5;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
@@ -578,7 +578,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 15.5;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
diff --git a/example/lib/trimmer_view.dart b/example/lib/trimmer_view.dart
index 1fcf6c09..515eba3f 100644
--- a/example/lib/trimmer_view.dart
+++ b/example/lib/trimmer_view.dart
@@ -69,70 +69,71 @@ class _TrimmerViewState extends State {
appBar: AppBar(
title: const Text("Video Trimmer"),
),
- body: Builder(
- builder: (context) => Center(
- child: Container(
- padding: const EdgeInsets.only(bottom: 30.0),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- mainAxisSize: MainAxisSize.max,
- children: [
- Visibility(
- visible: _progressVisibility,
- child: const LinearProgressIndicator(
- backgroundColor: Colors.red,
- ),
- ),
- ElevatedButton(
- onPressed: _progressVisibility ? null : () => _saveVideo(),
- child: const Text("SAVE"),
- ),
- Expanded(
- child: VideoViewer(trimmer: _trimmer),
+ body: Center(
+ child: Container(
+ padding: const EdgeInsets.only(bottom: 30.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Visibility(
+ visible: _progressVisibility,
+ child: const LinearProgressIndicator(
+ backgroundColor: Colors.red,
),
- Center(
- child: TrimEditor(
+ ),
+ ElevatedButton(
+ onPressed: _progressVisibility ? null : () => _saveVideo(),
+ child: const Text("SAVE"),
+ ),
+ Expanded(
+ child: VideoViewer(trimmer: _trimmer),
+ ),
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: TrimViewer(
trimmer: _trimmer,
viewerHeight: 50.0,
viewerWidth: MediaQuery.of(context).size.width,
maxVideoLength: const Duration(seconds: 10),
- onChangeStart: (value) {
- _startValue = value;
- },
- onChangeEnd: (value) {
- _endValue = value;
- },
- onChangePlaybackState: (value) {
- setState(() {
- _isPlaying = value;
- });
- },
+ editorProperties: TrimEditorProperties(
+ borderPaintColor: Colors.yellow,
+ borderWidth: 4,
+ borderRadius: 5,
+ circlePaintColor: Colors.yellow.shade800,
+ ),
+ areaProperties: TrimAreaProperties.edgeBlur(
+ thumbnailQuality: 10,
+ ),
+ onChangeStart: (value) => _startValue = value,
+ onChangeEnd: (value) => _endValue = value,
+ onChangePlaybackState: (value) =>
+ setState(() => _isPlaying = value),
),
),
- TextButton(
- child: _isPlaying
- ? const Icon(
- Icons.pause,
- size: 80.0,
- color: Colors.white,
- )
- : const Icon(
- Icons.play_arrow,
- size: 80.0,
- color: Colors.white,
- ),
- onPressed: () async {
- bool playbackState = await _trimmer.videPlaybackControl(
- startValue: _startValue,
- endValue: _endValue,
- );
- setState(() {
- _isPlaying = playbackState;
- });
- },
- )
- ],
- ),
+ ),
+ TextButton(
+ child: _isPlaying
+ ? const Icon(
+ Icons.pause,
+ size: 80.0,
+ color: Colors.white,
+ )
+ : const Icon(
+ Icons.play_arrow,
+ size: 80.0,
+ color: Colors.white,
+ ),
+ onPressed: () async {
+ bool playbackState = await _trimmer.videPlaybackControl(
+ startValue: _startValue,
+ endValue: _endValue,
+ );
+ setState(() => _isPlaying = playbackState);
+ },
+ )
+ ],
),
),
),
diff --git a/example/pubspec.lock b/example/pubspec.lock
index d4346c7e..a060b986 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -226,6 +226,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
+ transparent_image:
+ dependency: transitive
+ description:
+ name: transparent_image
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
vector_math:
dependency: transitive
description:
@@ -281,7 +288,7 @@ packages:
path: ".."
relative: true
source: path
- version: "1.2.0"
+ version: "2.0.0"
win32:
dependency: transitive
description:
diff --git a/lib/src/thumbnail_viewer.dart b/lib/src/thumbnail_viewer.dart
deleted file mode 100644
index a916c0a3..00000000
--- a/lib/src/thumbnail_viewer.dart
+++ /dev/null
@@ -1,89 +0,0 @@
-import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:flutter/material.dart';
-import 'package:video_thumbnail/video_thumbnail.dart';
-
-class ThumbnailViewer extends StatelessWidget {
- final File videoFile;
- final int videoDuration;
- final double thumbnailHeight;
- final BoxFit fit;
- final int numberOfThumbnails;
- final int quality;
-
- /// For showing the thumbnails generated from the video,
- /// like a frame by frame preview
- const ThumbnailViewer({
- Key? key,
- required this.videoFile,
- required this.videoDuration,
- required this.thumbnailHeight,
- required this.numberOfThumbnails,
- required this.fit,
- this.quality = 75,
- }) : super(key: key);
-
- Stream> generateThumbnail() async* {
- final String videoPath = videoFile.path;
-
- double eachPart = videoDuration / numberOfThumbnails;
-
- List byteList = [];
-
- // the cache of last thumbnail
- Uint8List? lastBytes;
-
- for (int i = 1; i <= numberOfThumbnails; i++) {
- Uint8List? bytes;
- bytes = await VideoThumbnail.thumbnailData(
- video: videoPath,
- imageFormat: ImageFormat.JPEG,
- timeMs: (eachPart * i).toInt(),
- quality: quality,
- );
-
- // if current thumbnail is null use the last thumbnail
- if (bytes != null) {
- lastBytes = bytes;
- } else {
- bytes = lastBytes;
- }
-
- byteList.add(bytes);
-
- yield byteList;
- }
- }
-
- @override
- Widget build(BuildContext context) {
- return StreamBuilder>(
- stream: generateThumbnail(),
- builder: (context, snapshot) {
- if (snapshot.hasData) {
- List imageBytes = snapshot.data!;
- return ListView.builder(
- scrollDirection: Axis.horizontal,
- itemCount: imageBytes.length,
- itemBuilder: (context, index) {
- return SizedBox(
- height: thumbnailHeight,
- width: thumbnailHeight,
- child: Image(
- image: MemoryImage(imageBytes[index]!),
- fit: fit,
- ),
- );
- });
- } else {
- return Container(
- color: Colors.grey[900],
- height: thumbnailHeight,
- width: double.maxFinite,
- );
- }
- },
- );
- }
-}
diff --git a/lib/src/trim_viewer/fixed_viewer/fixed_thumbnail_viewer.dart b/lib/src/trim_viewer/fixed_viewer/fixed_thumbnail_viewer.dart
new file mode 100644
index 00000000..3d76a3d4
--- /dev/null
+++ b/lib/src/trim_viewer/fixed_viewer/fixed_thumbnail_viewer.dart
@@ -0,0 +1,108 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:transparent_image/transparent_image.dart';
+import 'package:video_thumbnail/video_thumbnail.dart';
+
+class FixedThumbnailViewer extends StatelessWidget {
+ final File videoFile;
+ final int videoDuration;
+ final double thumbnailHeight;
+ final BoxFit fit;
+ final int numberOfThumbnails;
+ final VoidCallback onThumbnailLoadingComplete;
+ final int quality;
+
+ /// For showing the thumbnails generated from the video,
+ /// like a frame by frame preview
+ const FixedThumbnailViewer({
+ Key? key,
+ required this.videoFile,
+ required this.videoDuration,
+ required this.thumbnailHeight,
+ required this.numberOfThumbnails,
+ required this.fit,
+ required this.onThumbnailLoadingComplete,
+ this.quality = 75,
+ }) : super(key: key);
+
+ Stream> generateThumbnail() async* {
+ final String videoPath = videoFile.path;
+ double eachPart = videoDuration / numberOfThumbnails;
+ List byteList = [];
+ // the cache of last thumbnail
+ Uint8List? lastBytes;
+ for (int i = 1; i <= numberOfThumbnails; i++) {
+ Uint8List? bytes;
+ try {
+ bytes = await VideoThumbnail.thumbnailData(
+ video: videoPath,
+ imageFormat: ImageFormat.JPEG,
+ timeMs: (eachPart * i).toInt(),
+ quality: quality,
+ );
+ } catch (e) {
+ debugPrint('ERROR: Couldn\'t generate thumbnails: $e');
+ }
+ // if current thumbnail is null use the last thumbnail
+ if (bytes != null) {
+ lastBytes = bytes;
+ } else {
+ bytes = lastBytes;
+ }
+ byteList.add(bytes);
+ if (byteList.length == numberOfThumbnails) {
+ onThumbnailLoadingComplete();
+ }
+ yield byteList;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return StreamBuilder>(
+ stream: generateThumbnail(),
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ List imageBytes = snapshot.data!;
+ return Row(
+ mainAxisSize: MainAxisSize.max,
+ children: List.generate(
+ numberOfThumbnails,
+ (index) => SizedBox(
+ height: thumbnailHeight,
+ width: thumbnailHeight,
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ Opacity(
+ opacity: 0.2,
+ child: Image.memory(
+ imageBytes[0] ?? kTransparentImage,
+ fit: fit,
+ ),
+ ),
+ index < imageBytes.length
+ ? FadeInImage(
+ placeholder: MemoryImage(kTransparentImage),
+ image: MemoryImage(imageBytes[index]!),
+ fit: fit,
+ )
+ : const SizedBox(),
+ ],
+ ),
+ ),
+ ),
+ );
+ } else {
+ return Container(
+ color: Colors.grey[900],
+ height: thumbnailHeight,
+ width: double.maxFinite,
+ );
+ }
+ },
+ );
+ }
+}
diff --git a/lib/src/trim_editor.dart b/lib/src/trim_viewer/fixed_viewer/fixed_trim_viewer.dart
similarity index 61%
rename from lib/src/trim_editor.dart
rename to lib/src/trim_viewer/fixed_viewer/fixed_trim_viewer.dart
index 84bae2c7..0bcba00d 100644
--- a/lib/src/trim_editor.dart
+++ b/lib/src/trim_viewer/fixed_viewer/fixed_trim_viewer.dart
@@ -1,12 +1,18 @@
+import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
import 'package:video_player/video_player.dart';
-import 'package:video_trimmer/src/thumbnail_viewer.dart';
-import 'package:video_trimmer/src/trim_editor_painter.dart';
+import 'package:video_trimmer/src/trim_viewer/trim_editor_painter.dart';
import 'package:video_trimmer/src/trimmer.dart';
-class TrimEditor extends StatefulWidget {
+import '../../utils/editor_drag_type.dart';
+import '../trim_area_properties.dart';
+import '../trim_editor_properties.dart';
+import 'fixed_thumbnail_viewer.dart';
+
+class FixedTrimViewer extends StatefulWidget {
/// The Trimmer instance controlling the data.
final Trimmer trimmer;
@@ -16,56 +22,9 @@ class TrimEditor extends StatefulWidget {
/// For defining the total trimmer area height
final double viewerHeight;
- /// For defining the image fit type of each thumbnail image.
- ///
- /// By default it is set to `BoxFit.fitHeight`.
- final BoxFit fit;
-
/// For defining the maximum length of the output video.
final Duration maxVideoLength;
- /// For specifying a size to the holder at the
- /// two ends of the video trimmer area, while it is `idle`.
- ///
- /// By default it is set to `5.0`.
- final double circleSize;
-
- /// For specifying the width of the border around
- /// the trim area. By default it is set to `3`.
- final double borderWidth;
-
- /// For specifying the width of the video scrubber
- final double scrubberWidth;
-
- /// For specifying a size to the holder at
- /// the two ends of the video trimmer area, while it is being
- /// `dragged`.
- ///
- /// By default it is set to `8.0`.
- final double circleSizeOnDrag;
-
- /// For specifying a color to the circle.
- ///
- /// By default it is set to `Colors.white`.
- final Color circlePaintColor;
-
- /// For specifying a color to the border of
- /// the trim area.
- ///
- /// By default it is set to `Colors.white`.
- final Color borderPaintColor;
-
- /// For specifying a color to the video
- /// scrubber inside the trim area.
- ///
- /// By default it is set to `Colors.white`.
- final Color scrubberPaintColor;
-
- /// For specifying the quality of each
- /// generated image thumbnail, to be displayed in the trimmer
- /// area.
- final int thumbnailQuality;
-
/// For showing the start and the end point of the
/// video on top of the trimmer area.
///
@@ -95,10 +54,13 @@ class TrimEditor extends StatefulWidget {
/// playing, otherwise paused.
final Function(bool isPlaying)? onChangePlaybackState;
- /// Determines the touch size of the side handles, left and right. The rest, in
- /// the center, will move the whole frame if [maxVideoLength] is inferior to the
- /// total duration of the video.
- final int sideTapSize;
+ /// Properties for customizing the trim editor.
+ final TrimEditorProperties editorProperties;
+
+ /// Properties for customizing the fixed trim area.
+ final FixedTrimAreaProperties areaProperties;
+
+ final VoidCallback onThumbnailLoadingComplete;
/// Widget for displaying the video trimmer.
///
@@ -116,42 +78,10 @@ class TrimEditor extends StatefulWidget {
///
/// The optional parameters are:
///
- /// * [fit] for specifying the image fit type of each thumbnail image.
- /// By default it is set to `BoxFit.fitHeight`.
- ///
- ///
/// * [maxVideoLength] for specifying the maximum length of the
/// output video.
///
///
- /// * [circleSize] for specifying a size to the holder at the
- /// two ends of the video trimmer area, while it is `idle`.
- /// By default it is set to `5.0`.
- ///
- ///
- /// * [circleSizeOnDrag] for specifying a size to the holder at
- /// the two ends of the video trimmer area, while it is being
- /// `dragged`. By default it is set to `8.0`.
- ///
- ///
- /// * [circlePaintColor] for specifying a color to the circle.
- /// By default it is set to `Colors.white`.
- ///
- ///
- /// * [borderPaintColor] for specifying a color to the border of
- /// the trim area. By default it is set to `Colors.white`.
- ///
- ///
- /// * [scrubberPaintColor] for specifying a color to the video
- /// scrubber inside the trim area. By default it is set to
- /// `Colors.white`.
- ///
- ///
- /// * [thumbnailQuality] for specifying the quality of each
- /// generated image thumbnail, to be displayed in the trimmer
- /// area.
- ///
- ///
/// * [showDuration] for showing the start and the end point of the
/// video on top of the trimmer area. By default it is set to `true`.
///
@@ -170,34 +100,35 @@ class TrimEditor extends StatefulWidget {
/// * [onChangePlaybackState] is a callback to the video playback
/// state to know whether it is currently playing or paused.
///
- const TrimEditor({
- Key? key,
+ ///
+ /// * [editorProperties] defines properties for customizing the trim editor.
+ ///
+ ///
+ /// * [areaProperties] defines properties for customizing the fixed trim area.
+ ///
+ const FixedTrimViewer({
+ super.key,
required this.trimmer,
+ required this.onThumbnailLoadingComplete,
this.viewerWidth = 50.0 * 8,
this.viewerHeight = 50,
- this.fit = BoxFit.fitHeight,
this.maxVideoLength = const Duration(milliseconds: 0),
- this.circleSize = 5.0,
- this.borderWidth = 3,
- this.scrubberWidth = 1,
- this.circleSizeOnDrag = 8.0,
- this.circlePaintColor = Colors.white,
- this.borderPaintColor = Colors.white,
- this.scrubberPaintColor = Colors.white,
- this.thumbnailQuality = 75,
this.showDuration = true,
- this.sideTapSize = 24,
this.durationTextStyle = const TextStyle(color: Colors.white),
this.onChangeStart,
this.onChangeEnd,
this.onChangePlaybackState,
- }) : super(key: key);
+ this.editorProperties = const TrimEditorProperties(),
+ this.areaProperties = const FixedTrimAreaProperties(),
+ });
@override
- State createState() => _TrimEditorState();
+ State createState() => _FixedTrimViewerState();
}
-class _TrimEditorState extends State with TickerProviderStateMixin {
+class _FixedTrimViewerState extends State
+ with TickerProviderStateMixin {
+ final _trimmerAreaKey = GlobalKey();
File? get _videoFile => widget.trimmer.currentVideoFile;
double _videoStartPos = 0.0;
@@ -217,12 +148,14 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
int _numberOfThumbnails = 0;
- late double _circleSize;
+ late double _startCircleSize;
+ late double _endCircleSize;
+ late double _borderRadius;
double? fraction;
double? maxLengthPixels;
- ThumbnailViewer? thumbnailWidget;
+ FixedThumbnailViewer? thumbnailWidget;
Animation? _scrubberAnimation;
AnimationController? _animationController;
@@ -244,67 +177,80 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
@override
void initState() {
super.initState();
-
- widget.trimmer.eventStream.listen((event) {
- if (event == TrimmerEvent.initialized) {
- //The video has been initialized, now we can load stuff
-
- _initializeVideoController();
- videoPlayerController.seekTo(const Duration(milliseconds: 0));
- setState(() {
- Duration totalDuration = videoPlayerController.value.duration;
-
- if (widget.maxVideoLength > const Duration(milliseconds: 0) &&
- widget.maxVideoLength < totalDuration) {
- if (widget.maxVideoLength < totalDuration) {
- fraction = widget.maxVideoLength.inMilliseconds /
- totalDuration.inMilliseconds;
-
- maxLengthPixels = _thumbnailViewerW * fraction!;
- }
- } else {
- maxLengthPixels = _thumbnailViewerW;
+ _startCircleSize = widget.editorProperties.circleSize;
+ _endCircleSize = widget.editorProperties.circleSize;
+ _borderRadius = widget.editorProperties.borderRadius;
+ _thumbnailViewerH = widget.viewerHeight;
+ log('thumbnailViewerW: $_thumbnailViewerW');
+ SchedulerBinding.instance.addPostFrameCallback((_) {
+ final renderBox =
+ _trimmerAreaKey.currentContext?.findRenderObject() as RenderBox?;
+ final trimmerActualWidth = renderBox?.size.width;
+ log('RENDER BOX: $trimmerActualWidth');
+ if (trimmerActualWidth == null) return;
+ _thumbnailViewerW = trimmerActualWidth;
+ _initializeVideoController();
+ videoPlayerController.seekTo(const Duration(milliseconds: 0));
+ _numberOfThumbnails = trimmerActualWidth ~/ _thumbnailViewerH;
+ log('numberOfThumbnails: $_numberOfThumbnails');
+ log('thumbnailViewerW: $_thumbnailViewerW');
+ setState(() {
+ _thumbnailViewerW = _numberOfThumbnails * _thumbnailViewerH;
+
+ final FixedThumbnailViewer thumbnailWidget = FixedThumbnailViewer(
+ videoFile: _videoFile!,
+ videoDuration: _videoDuration,
+ fit: widget.areaProperties.thumbnailFit,
+ thumbnailHeight: _thumbnailViewerH,
+ numberOfThumbnails: _numberOfThumbnails,
+ quality: widget.areaProperties.thumbnailQuality,
+ onThumbnailLoadingComplete: widget.onThumbnailLoadingComplete,
+ );
+ this.thumbnailWidget = thumbnailWidget;
+ Duration totalDuration = videoPlayerController.value.duration;
+
+ if (widget.maxVideoLength > const Duration(milliseconds: 0) &&
+ widget.maxVideoLength < totalDuration) {
+ if (widget.maxVideoLength < totalDuration) {
+ fraction = widget.maxVideoLength.inMilliseconds /
+ totalDuration.inMilliseconds;
+
+ maxLengthPixels = _thumbnailViewerW * fraction!;
}
+ } else {
+ maxLengthPixels = _thumbnailViewerW;
+ }
- _videoEndPos = fraction != null
- ? _videoDuration.toDouble() * fraction!
- : _videoDuration.toDouble();
-
- widget.onChangeEnd!(_videoEndPos);
-
- _endPos = Offset(
- maxLengthPixels != null ? maxLengthPixels! : _thumbnailViewerW,
- _thumbnailViewerH,
- );
-
- // Defining the tween points
- _linearTween = Tween(begin: _startPos.dx, end: _endPos.dx);
- _animationController = AnimationController(
- vsync: this,
- duration:
- Duration(milliseconds: (_videoEndPos - _videoStartPos).toInt()),
- );
-
- _scrubberAnimation = _linearTween.animate(_animationController!)
- ..addListener(() {
- setState(() {});
- })
- ..addStatusListener((status) {
- if (status == AnimationStatus.completed) {
- _animationController!.stop();
- }
- });
- });
- }
+ _videoEndPos = fraction != null
+ ? _videoDuration.toDouble() * fraction!
+ : _videoDuration.toDouble();
+
+ widget.onChangeEnd!(_videoEndPos);
+
+ _endPos = Offset(
+ maxLengthPixels != null ? maxLengthPixels! : _thumbnailViewerW,
+ _thumbnailViewerH,
+ );
+
+ // Defining the tween points
+ _linearTween = Tween(begin: _startPos.dx, end: _endPos.dx);
+ _animationController = AnimationController(
+ vsync: this,
+ duration:
+ Duration(milliseconds: (_videoEndPos - _videoStartPos).toInt()),
+ );
+
+ _scrubberAnimation = _linearTween.animate(_animationController!)
+ ..addListener(() {
+ setState(() {});
+ })
+ ..addStatusListener((status) {
+ if (status == AnimationStatus.completed) {
+ _animationController!.stop();
+ }
+ });
+ });
});
-
- _circleSize = widget.circleSize;
-
- _thumbnailViewerH = widget.viewerHeight;
-
- _numberOfThumbnails = widget.viewerWidth ~/ _thumbnailViewerH;
-
- _thumbnailViewerW = _numberOfThumbnails * _thumbnailViewerH;
}
Future _initializeVideoController() async {
@@ -345,16 +291,6 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
videoPlayerController.setVolume(1.0);
_videoDuration = videoPlayerController.value.duration.inMilliseconds;
-
- final ThumbnailViewer thumbnailWidget = ThumbnailViewer(
- videoFile: _videoFile!,
- videoDuration: _videoDuration,
- fit: widget.fit,
- thumbnailHeight: _thumbnailViewerH,
- numberOfThumbnails: _numberOfThumbnails,
- quality: widget.thumbnailQuality,
- );
- this.thumbnailWidget = thumbnailWidget;
}
}
@@ -369,10 +305,10 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
final startDifference = _startPos.dx - details.localPosition.dx;
final endDifference = _endPos.dx - details.localPosition.dx;
- //First we determine whether the dragging motion should be allowed. The allowed
- //zone is widget.sideTapSize (left) + frame (center) + widget.sideTapSize (right)
- if (startDifference <= widget.sideTapSize &&
- endDifference >= -widget.sideTapSize) {
+ // First we determine whether the dragging motion should be allowed. The allowed
+ // zone is widget.sideTapSize (left) + frame (center) + widget.sideTapSize (right)
+ if (startDifference <= widget.editorProperties.sideTapSize &&
+ endDifference >= -widget.editorProperties.sideTapSize) {
_allowDrag = true;
} else {
debugPrint("Dragging is outside of frame, ignoring gesture...");
@@ -380,10 +316,12 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
return;
}
- //Now we determine which part is dragged
- if (details.localPosition.dx <= _startPos.dx + widget.sideTapSize) {
+ // Now we determine which part is dragged
+ if (details.localPosition.dx <=
+ _startPos.dx + widget.editorProperties.sideTapSize) {
_dragType = EditorDragType.left;
- } else if (details.localPosition.dx <= _endPos.dx - widget.sideTapSize) {
+ } else if (details.localPosition.dx <=
+ _endPos.dx - widget.editorProperties.sideTapSize) {
_dragType = EditorDragType.center;
} else {
_dragType = EditorDragType.right;
@@ -396,9 +334,8 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
void _onDragUpdate(DragUpdateDetails details) {
if (!_allowDrag) return;
- _circleSize = widget.circleSizeOnDrag;
-
if (_dragType == EditorDragType.left) {
+ _startCircleSize = widget.editorProperties.circleSizeOnDrag;
if ((_startPos.dx + details.delta.dx >= 0) &&
(_startPos.dx + details.delta.dx <= _endPos.dx) &&
!(_endPos.dx - _startPos.dx - details.delta.dx > maxLengthPixels!)) {
@@ -406,6 +343,8 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
_onStartDragged();
}
} else if (_dragType == EditorDragType.center) {
+ _startCircleSize = widget.editorProperties.circleSizeOnDrag;
+ _endCircleSize = widget.editorProperties.circleSizeOnDrag;
if ((_startPos.dx + details.delta.dx >= 0) &&
(_endPos.dx + details.delta.dx <= _thumbnailViewerW)) {
_startPos += details.delta;
@@ -414,6 +353,7 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
_onEndDragged();
}
} else {
+ _endCircleSize = widget.editorProperties.circleSizeOnDrag;
if ((_endPos.dx + details.delta.dx <= _thumbnailViewerW) &&
(_endPos.dx + details.delta.dx >= _startPos.dx) &&
!(_endPos.dx - _startPos.dx + details.delta.dx > maxLengthPixels!)) {
@@ -447,7 +387,8 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
/// Drag gesture ended, update UI accordingly.
void _onDragEnd(DragEndDetails details) {
setState(() {
- _circleSize = widget.circleSize;
+ _startCircleSize = widget.editorProperties.circleSize;
+ _endCircleSize = widget.editorProperties.circleSize;
if (_dragType == EditorDragType.right) {
videoPlayerController
.seekTo(Duration(milliseconds: _videoEndPos.toInt()));
@@ -518,18 +459,27 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
startPos: _startPos,
endPos: _endPos,
scrubberAnimationDx: _scrubberAnimation?.value ?? 0,
- circleSize: _circleSize,
- borderWidth: widget.borderWidth,
- scrubberWidth: widget.scrubberWidth,
- circlePaintColor: widget.circlePaintColor,
- borderPaintColor: widget.borderPaintColor,
- scrubberPaintColor: widget.scrubberPaintColor,
+ startCircleSize: _startCircleSize,
+ endCircleSize: _endCircleSize,
+ borderRadius: _borderRadius,
+ borderWidth: widget.editorProperties.borderWidth,
+ scrubberWidth: widget.editorProperties.scrubberWidth,
+ circlePaintColor: widget.editorProperties.circlePaintColor,
+ borderPaintColor: widget.editorProperties.borderPaintColor,
+ scrubberPaintColor: widget.editorProperties.scrubberPaintColor,
),
- child: Container(
- color: Colors.grey[900],
- height: _thumbnailViewerH,
- width: _thumbnailViewerW,
- child: thumbnailWidget ?? Container(),
+ child: ClipRRect(
+ borderRadius:
+ BorderRadius.circular(widget.areaProperties.borderRadius),
+ child: Container(
+ key: _trimmerAreaKey,
+ color: Colors.grey[900],
+ height: _thumbnailViewerH,
+ width: _thumbnailViewerW == 0.0
+ ? widget.viewerWidth
+ : _thumbnailViewerW,
+ child: thumbnailWidget ?? Container(),
+ ),
),
),
],
@@ -537,14 +487,3 @@ class _TrimEditorState extends State with TickerProviderStateMixin {
);
}
}
-
-enum EditorDragType {
- /// The user is dragging the left part of the frame.
- left,
-
- /// The user is dragging the whole frame.
- center,
-
- /// The user is dragging the right part of the frame.
- right
-}
diff --git a/lib/src/trim_viewer/scrollable_viewer/scrollable_thumbnail_viewer.dart b/lib/src/trim_viewer/scrollable_viewer/scrollable_thumbnail_viewer.dart
new file mode 100644
index 00000000..8207d8ca
--- /dev/null
+++ b/lib/src/trim_viewer/scrollable_viewer/scrollable_thumbnail_viewer.dart
@@ -0,0 +1,120 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:transparent_image/transparent_image.dart';
+import 'package:video_thumbnail/video_thumbnail.dart';
+
+class ScrollableThumbnailViewer extends StatelessWidget {
+ final File videoFile;
+ final int videoDuration;
+ final double thumbnailHeight;
+ final BoxFit fit;
+ final int numberOfThumbnails;
+ final int quality;
+ final ScrollController scrollController;
+ final VoidCallback onThumbnailLoadingComplete;
+
+ /// For showing the thumbnails generated from the video,
+ /// like a frame by frame preview
+ const ScrollableThumbnailViewer({
+ Key? key,
+ required this.videoFile,
+ required this.videoDuration,
+ required this.thumbnailHeight,
+ required this.numberOfThumbnails,
+ required this.fit,
+ required this.scrollController,
+ required this.onThumbnailLoadingComplete,
+ this.quality = 75,
+ }) : super(key: key);
+
+ Stream> generateThumbnail() async* {
+ final String videoPath = videoFile.path;
+ double eachPart = videoDuration / numberOfThumbnails;
+ List byteList = [];
+ // the cache of last thumbnail
+ Uint8List? lastBytes;
+ for (int i = 1; i <= numberOfThumbnails; i++) {
+ Uint8List? bytes;
+ try {
+ bytes = await VideoThumbnail.thumbnailData(
+ video: videoPath,
+ imageFormat: ImageFormat.JPEG,
+ timeMs: (eachPart * i).toInt(),
+ quality: quality,
+ );
+ } catch (e) {
+ debugPrint('ERROR: Couldn\'t generate thumbnails: $e');
+ }
+ // if current thumbnail is null use the last thumbnail
+ if (bytes != null) {
+ lastBytes = bytes;
+ } else {
+ bytes = lastBytes;
+ }
+ byteList.add(bytes);
+ if (byteList.length == numberOfThumbnails) {
+ onThumbnailLoadingComplete();
+ }
+ yield byteList;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return IgnorePointer(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ controller: scrollController,
+ child: SizedBox(
+ width: numberOfThumbnails * thumbnailHeight,
+ height: thumbnailHeight,
+ child: StreamBuilder>(
+ stream: generateThumbnail(),
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ List imageBytes = snapshot.data!;
+ return Row(
+ mainAxisSize: MainAxisSize.max,
+ children: List.generate(
+ numberOfThumbnails,
+ (index) => SizedBox(
+ height: thumbnailHeight,
+ width: thumbnailHeight,
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ Opacity(
+ opacity: 0.2,
+ child: Image.memory(
+ imageBytes[0] ?? kTransparentImage,
+ fit: fit,
+ ),
+ ),
+ index < imageBytes.length
+ ? FadeInImage(
+ placeholder: MemoryImage(kTransparentImage),
+ image: MemoryImage(imageBytes[index]!),
+ fit: fit,
+ )
+ : const SizedBox(),
+ ],
+ ),
+ ),
+ ),
+ );
+ } else {
+ return Container(
+ color: Colors.grey[900],
+ height: thumbnailHeight,
+ width: double.maxFinite,
+ );
+ }
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/trim_viewer/scrollable_viewer/scrollable_trim_viewer.dart b/lib/src/trim_viewer/scrollable_viewer/scrollable_trim_viewer.dart
new file mode 100644
index 00000000..bc6f78af
--- /dev/null
+++ b/lib/src/trim_viewer/scrollable_viewer/scrollable_trim_viewer.dart
@@ -0,0 +1,730 @@
+import 'dart:async';
+import 'dart:developer';
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:video_player/video_player.dart';
+import 'package:video_trimmer/src/trim_viewer/trim_editor_painter.dart';
+import 'package:video_trimmer/src/trim_viewer/trim_area_properties.dart';
+import 'package:video_trimmer/src/trim_viewer/trim_editor_properties.dart';
+import 'package:video_trimmer/src/trimmer.dart';
+
+import '../../utils/editor_drag_type.dart';
+import 'scrollable_thumbnail_viewer.dart';
+
+class ScrollableTrimViewer extends StatefulWidget {
+ /// The Trimmer instance controlling the data.
+ final Trimmer trimmer;
+
+ /// For defining the total trimmer area width
+ final double viewerWidth;
+
+ /// For defining the total trimmer area height
+ final double viewerHeight;
+
+ /// For defining the maximum length of the output video.
+ final Duration maxVideoLength;
+
+ /// For showing the start and the end point of the
+ /// video on top of the trimmer area.
+ ///
+ /// By default it is set to `true`.
+ final bool showDuration;
+
+ /// For providing a `TextStyle` to the
+ /// duration text.
+ ///
+ /// By default it is set to `TextStyle(color: Colors.white)`
+ final TextStyle durationTextStyle;
+
+ /// Callback to the video start position
+ ///
+ /// Returns the selected video start position in `milliseconds`.
+ final Function(double startValue)? onChangeStart;
+
+ /// Callback to the video end position.
+ ///
+ /// Returns the selected video end position in `milliseconds`.
+ final Function(double endValue)? onChangeEnd;
+
+ /// Callback to the video playback
+ /// state to know whether it is currently playing or paused.
+ ///
+ /// Returns a `boolean` value. If `true`, video is currently
+ /// playing, otherwise paused.
+ final Function(bool isPlaying)? onChangePlaybackState;
+
+ /// This is the fraction of padding present beside the trimmer editor,
+ /// calculated on the `maxVideoLength` value.
+ final double paddingFraction;
+
+ /// Properties for customizing the trim editor.
+ final TrimEditorProperties editorProperties;
+
+ /// Properties for customizing the trim area.
+ final TrimAreaProperties areaProperties;
+
+ final VoidCallback onThumbnailLoadingComplete;
+
+ /// Widget for displaying the video trimmer.
+ ///
+ /// This has frame wise preview of the video with a
+ /// slider for selecting the part of the video to be
+ /// trimmed.
+ ///
+ /// The required parameters are [viewerWidth] & [viewerHeight]
+ ///
+ /// * [viewerWidth] to define the total trimmer area width.
+ ///
+ ///
+ /// * [viewerHeight] to define the total trimmer area height.
+ ///
+ ///
+ /// The optional parameters are:
+ ///
+ /// * [maxVideoLength] for specifying the maximum length of the
+ /// output video.
+ ///
+ ///
+ /// * [showDuration] for showing the start and the end point of the
+ /// video on top of the trimmer area. By default it is set to `true`.
+ ///
+ ///
+ /// * [durationTextStyle] is for providing a `TextStyle` to the
+ /// duration text. By default it is set to
+ /// `TextStyle(color: Colors.white)`
+ ///
+ ///
+ /// * [onChangeStart] is a callback to the video start position.
+ ///
+ ///
+ /// * [onChangeEnd] is a callback to the video end position.
+ ///
+ ///
+ /// * [onChangePlaybackState] is a callback to the video playback
+ /// state to know whether it is currently playing or paused.
+ ///
+ ///
+ /// * [editorProperties] defines properties for customizing the trim editor.
+ ///
+ ///
+ /// * [areaProperties] defines properties for customizing the trim area.
+ ///
+ const ScrollableTrimViewer({
+ super.key,
+ required this.trimmer,
+ required this.maxVideoLength,
+ required this.onThumbnailLoadingComplete,
+ this.viewerWidth = 50 * 8,
+ this.viewerHeight = 50,
+ this.showDuration = true,
+ this.durationTextStyle = const TextStyle(color: Colors.white),
+ this.onChangeStart,
+ this.onChangeEnd,
+ this.onChangePlaybackState,
+ this.paddingFraction = 0.2,
+ this.editorProperties = const TrimEditorProperties(),
+ this.areaProperties = const TrimAreaProperties(),
+ });
+
+ @override
+ State createState() => _ScrollableTrimViewerState();
+}
+
+class _ScrollableTrimViewerState extends State
+ with TickerProviderStateMixin {
+ final _trimmerAreaKey = GlobalKey();
+ File? get _videoFile => widget.trimmer.currentVideoFile;
+
+ double _videoStartPos = 0.0;
+ double _videoEndPos = 0.0;
+
+ double _localPosition = 0.0;
+
+ Offset _startPos = const Offset(0, 0);
+ Offset _endPos = const Offset(0, 0);
+
+ double _startFraction = 0.0;
+ double _endFraction = 1.0;
+
+ int _videoDuration = 0;
+ int _currentPosition = 0;
+ int _trimmerAreaDuration = 0;
+ int _remainingDuration = 0;
+
+ double _thumbnailViewerW = 0.0;
+ double _thumbnailViewerH = 0.0;
+
+ int _numberOfThumbnails = 0;
+
+ double _autoStartScrollPos = 0.0;
+ double _autoEndScrollPos = 0.0;
+
+ late double _startCircleSize;
+ late double _endCircleSize;
+ late double _borderRadius;
+
+ double? fraction;
+ double? maxLengthPixels;
+
+ ScrollableThumbnailViewer? thumbnailWidget;
+
+ Animation? _scrubberAnimation;
+ AnimationController? _animationController;
+ late Tween _linearTween;
+
+ /// Quick access to VideoPlayerController, only not null after [TrimmerEvent.initialized]
+ /// has been emitted.
+ VideoPlayerController get videoPlayerController =>
+ widget.trimmer.videoPlayerController!;
+
+ /// Keep track of the drag type, e.g. whether the user drags the left, center or
+ /// right part of the frame. Set this in [_onDragStart] when the dragging starts.
+ EditorDragType _dragType = EditorDragType.left;
+
+ /// Whether the dragging is allowed. Dragging is ignore if the user's gesture is outside
+ /// of the frame, to make the UI more realistic.
+ bool _allowDrag = true;
+
+ late final ScrollController _scrollController;
+ double scrollByValue = 10.0;
+ double currentScrollValue = 0.0;
+ double totalVideoLengthInPixels = 0.0;
+
+ Timer? _scrollStartTimer;
+ Timer? _scrollingTimer;
+
+ void startScrolling(bool isTowardsEnd) {
+ _scrollingTimer =
+ Timer.periodic(const Duration(milliseconds: 300), (timer) {
+ setState(() {
+ final midPoint = (_endPos.dx - _startPos.dx) / 2;
+ var speedMultiplier = 1;
+ if (isTowardsEnd) {
+ if (_localPosition >= _endPos.dx) {
+ speedMultiplier = 5;
+ } else if (_localPosition > (midPoint + (midPoint * 2 / 3))) {
+ speedMultiplier = 4;
+ } else if (_localPosition > (midPoint + midPoint / 3)) {
+ speedMultiplier = 2;
+ }
+ log('End scroll speed: ${speedMultiplier}x');
+ if (_endPos.dx >= _autoEndScrollPos &&
+ currentScrollValue <= totalVideoLengthInPixels) {
+ currentScrollValue = math.min(
+ currentScrollValue + scrollByValue * speedMultiplier,
+ _numberOfThumbnails * _thumbnailViewerH);
+ } else {
+ _scrollingTimer?.cancel();
+ return;
+ }
+ } else {
+ if (_localPosition <= _startPos.dx) {
+ speedMultiplier = 5;
+ } else if (_localPosition < (midPoint - (midPoint * 2 / 3))) {
+ speedMultiplier = 4;
+ } else if (_localPosition < (midPoint - midPoint / 3)) {
+ speedMultiplier = 2;
+ }
+ log('Start scroll speed: ${speedMultiplier}x');
+ if (_startPos.dx <= _autoStartScrollPos && currentScrollValue != 0) {
+ currentScrollValue = math.max(
+ 0, currentScrollValue - scrollByValue * speedMultiplier);
+ } else {
+ _scrollingTimer?.cancel();
+ return;
+ }
+ }
+ // log('scroll pixels: ${_scrollController.position.pixels}');
+ });
+
+ log('SCROLL: $currentScrollValue, (${((_scrollController.position.pixels / _scrollController.position.maxScrollExtent) * 100).toStringAsFixed(2)}%)');
+ _scrollController.animateTo(
+ currentScrollValue,
+ curve: Curves.easeOut,
+ duration: const Duration(milliseconds: 100),
+ );
+ final durationChange = (_scrollController.position.pixels /
+ _scrollController.position.maxScrollExtent) *
+ _remainingDuration;
+ _videoStartPos = (_trimmerAreaDuration * _startFraction) + durationChange;
+ _videoEndPos = (_trimmerAreaDuration * _endFraction) + durationChange;
+ });
+ setState(() {});
+ }
+
+ void startTimer(bool isTowardsEnd) {
+ var start = 300;
+ _scrollStartTimer = Timer.periodic(
+ const Duration(milliseconds: 100),
+ (Timer timer) {
+ if (start == 0) {
+ timer.cancel();
+ log('ANIMATE');
+ if (_scrollingTimer?.isActive ?? false) return;
+ startScrolling(isTowardsEnd);
+ } else {
+ start -= 100;
+ }
+ },
+ );
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _scrollController = ScrollController();
+ _startCircleSize = widget.editorProperties.circleSize;
+ _endCircleSize = widget.editorProperties.circleSize;
+ _borderRadius = widget.editorProperties.borderRadius;
+ _thumbnailViewerH = widget.viewerHeight;
+ SchedulerBinding.instance.addPostFrameCallback((_) {
+ final renderBox =
+ _trimmerAreaKey.currentContext?.findRenderObject() as RenderBox?;
+ final trimmerActualWidth = renderBox?.size.width;
+ log('RENDER BOX: ${renderBox?.size.width}');
+ if (trimmerActualWidth == null) return;
+ _thumbnailViewerW = trimmerActualWidth;
+ _initializeVideoController();
+ // The video has been initialized, now we can load stuff
+ videoPlayerController.seekTo(const Duration(milliseconds: 0));
+ setState(() {
+ final totalDuration = videoPlayerController.value.duration;
+ log('Total Video Length: $totalDuration');
+ final maxVideoLength = widget.maxVideoLength;
+ log('Max Video Length: $maxVideoLength');
+ final paddingFraction = widget.paddingFraction;
+ log('Padding Fraction: $paddingFraction');
+ // trimAreaTime = maxVideoLength + (paddingFraction * maxVideoLength) * 2
+ final trimAreaDuration = Duration(
+ milliseconds: (maxVideoLength.inMilliseconds +
+ ((paddingFraction * maxVideoLength.inMilliseconds) * 2)
+ .toInt()));
+ log('Trim Area Duration: $trimAreaDuration');
+ final remainingDuration = totalDuration - trimAreaDuration;
+ log('Remaining Duration: $remainingDuration');
+ _remainingDuration = remainingDuration.inMilliseconds;
+ final trimAreaLength = _thumbnailViewerW;
+ log('TRIM AREA LENGTH: $trimAreaLength');
+ final autoScrollAreaLength = trimAreaLength * 0.02;
+ log('autoScrollAreaLength: $autoScrollAreaLength');
+ _autoStartScrollPos = autoScrollAreaLength;
+ _autoEndScrollPos = trimAreaLength - autoScrollAreaLength;
+ log('autoStartScrollPos: $_autoStartScrollPos, autoEndScrollPos: $_autoEndScrollPos');
+ final thumbnailHeight = widget.viewerHeight;
+ final numberOfThumbnailsInArea = trimAreaLength / thumbnailHeight;
+ final numberOfThumbnailsTotal = (numberOfThumbnailsInArea *
+ (totalDuration.inMilliseconds /
+ trimAreaDuration.inMilliseconds))
+ .toInt();
+ log('THUMBNAILS: in area=$numberOfThumbnailsInArea, total=$numberOfThumbnailsTotal');
+
+ // find precise durations according to the number of thumbnails;
+ // preciseTotalLength = numberOfThumbnailsTotal * thumbnailHeight
+ // totalDuration => preciseTotalLength
+ // areaDuration => (preciseTotalLength * areaDuration) / totalDuration
+ _numberOfThumbnails = numberOfThumbnailsTotal;
+ final thumbnailWidget = ScrollableThumbnailViewer(
+ scrollController: _scrollController,
+ videoFile: _videoFile!,
+ videoDuration: _videoDuration,
+ fit: widget.areaProperties.thumbnailFit,
+ thumbnailHeight: _thumbnailViewerH,
+ numberOfThumbnails: _numberOfThumbnails,
+ quality: widget.areaProperties.thumbnailQuality,
+ onThumbnailLoadingComplete: widget.onThumbnailLoadingComplete,
+ );
+ this.thumbnailWidget = thumbnailWidget;
+ log('=========================');
+ final preciseTotalLength = numberOfThumbnailsTotal * thumbnailHeight;
+ log('preciseTotalLength: $preciseTotalLength');
+ totalVideoLengthInPixels = preciseTotalLength - trimAreaLength;
+ log('totalVideoLengthInPixels: $totalVideoLengthInPixels');
+ final preciseAreaDuration = Duration(
+ milliseconds: (totalDuration.inMilliseconds * trimAreaLength) ~/
+ preciseTotalLength);
+ _trimmerAreaDuration = preciseAreaDuration.inMilliseconds;
+ log('preciseAreaDuration: $preciseAreaDuration');
+ final trimmerFraction =
+ maxVideoLength.inMilliseconds / preciseAreaDuration.inMilliseconds;
+ log('trimmerFraction: $trimmerFraction');
+ final trimmerCover = trimmerFraction * trimAreaLength;
+ maxLengthPixels = trimmerCover;
+ _endPos = Offset(trimmerCover, thumbnailHeight);
+ log('START: $_startPos, END: $_endPos');
+
+ _videoEndPos =
+ preciseAreaDuration.inMilliseconds.toDouble() * trimmerFraction;
+ log('Video End Pos: $_videoEndPos ms');
+ widget.onChangeEnd!(_videoEndPos);
+ log('Video Selected Duration: ${_videoEndPos - _videoStartPos}');
+
+ // Defining the tween points
+ _linearTween = Tween(begin: _startPos.dx, end: _endPos.dx);
+ _animationController = AnimationController(
+ vsync: this,
+ duration:
+ Duration(milliseconds: (_videoEndPos - _videoStartPos).toInt()),
+ );
+
+ _scrubberAnimation = _linearTween.animate(_animationController!)
+ ..addListener(() {
+ setState(() {});
+ })
+ ..addStatusListener((status) {
+ if (status == AnimationStatus.completed) {
+ _animationController!.stop();
+ }
+ });
+ });
+ });
+ }
+
+ Future _initializeVideoController() async {
+ if (_videoFile == null) return;
+ videoPlayerController.addListener(() {
+ final bool isPlaying = videoPlayerController.value.isPlaying;
+
+ if (isPlaying) {
+ widget.onChangePlaybackState!(true);
+ setState(() {
+ _currentPosition =
+ videoPlayerController.value.position.inMilliseconds;
+
+ if (_currentPosition > _videoEndPos.toInt()) {
+ videoPlayerController.pause();
+ widget.onChangePlaybackState!(false);
+ _animationController!.stop();
+ } else {
+ if (!_animationController!.isAnimating) {
+ widget.onChangePlaybackState!(true);
+ _animationController!.forward();
+ }
+ }
+ });
+ } else {
+ if (videoPlayerController.value.isInitialized) {
+ if (_animationController != null) {
+ if ((_scrubberAnimation?.value ?? 0).toInt() ==
+ (_endPos.dx).toInt()) {
+ _animationController!.reset();
+ }
+ _animationController!.stop();
+ widget.onChangePlaybackState!(false);
+ }
+ }
+ }
+ });
+
+ videoPlayerController.setVolume(1.0);
+ _videoDuration = videoPlayerController.value.duration.inMilliseconds;
+ }
+
+ /// Called when the user starts dragging the frame, on either side on the whole frame.
+ /// Determine which [EditorDragType] is used.
+ void _onDragStart(DragStartDetails details) {
+ log("onDragStart");
+ log(details.localPosition.toString());
+ log((_startPos.dx - details.localPosition.dx).abs().toString());
+ log((_endPos.dx - details.localPosition.dx).abs().toString());
+
+ final startDifference = _startPos.dx - details.localPosition.dx;
+ final endDifference = _endPos.dx - details.localPosition.dx;
+
+ // First we determine whether the dragging motion should be allowed. The allowed
+ // zone is widget.sideTapSize (left) + frame (center) + widget.sideTapSize (right)
+ if (startDifference <= widget.editorProperties.sideTapSize &&
+ endDifference >= -widget.editorProperties.sideTapSize) {
+ _allowDrag = true;
+ } else {
+ debugPrint("Dragging is outside of frame, ignoring gesture...");
+ _allowDrag = false;
+ return;
+ }
+
+ // Now we determine which part is dragged
+ if (details.localPosition.dx <=
+ _startPos.dx + widget.editorProperties.sideTapSize) {
+ _dragType = EditorDragType.left;
+ } else if (details.localPosition.dx <=
+ _endPos.dx - widget.editorProperties.sideTapSize) {
+ _dragType = EditorDragType.center;
+ } else {
+ _dragType = EditorDragType.right;
+ }
+ }
+
+ /// Called during dragging, only executed if [_allowDrag] was set to true in
+ /// [_onDragStart].
+ /// Makes sure the limits are respected.
+ void _onDragUpdate(DragUpdateDetails details) {
+ if (!_allowDrag) return;
+
+ // log('Local pos: ${details.localPosition}');
+ _localPosition = details.localPosition.dx;
+
+ if (_dragType == EditorDragType.left) {
+ _startCircleSize = widget.editorProperties.circleSizeOnDrag;
+ if ((_startPos.dx + details.delta.dx >= 0) &&
+ (_startPos.dx + details.delta.dx <= _endPos.dx) &&
+ !(_endPos.dx - _startPos.dx - details.delta.dx > maxLengthPixels!)) {
+ _startPos += details.delta;
+ _onStartDragged();
+ }
+ } else if (_dragType == EditorDragType.center) {
+ _startCircleSize = widget.editorProperties.circleSizeOnDrag;
+ _endCircleSize = widget.editorProperties.circleSizeOnDrag;
+ if ((_startPos.dx + details.delta.dx >= 0) &&
+ (_endPos.dx + details.delta.dx <= _thumbnailViewerW)) {
+ _startPos += details.delta;
+ _endPos += details.delta;
+ _onStartDragged();
+ _onEndDragged();
+ }
+ } else {
+ _endCircleSize = widget.editorProperties.circleSizeOnDrag;
+ if ((_endPos.dx + details.delta.dx <= _thumbnailViewerW) &&
+ (_endPos.dx + details.delta.dx >= _startPos.dx) &&
+ !(_endPos.dx - _startPos.dx + details.delta.dx > maxLengthPixels!)) {
+ _endPos += details.delta;
+ _onEndDragged();
+ }
+ }
+ // log('Video Duration :: Start: ${_videoStartPos / 1000}ms, End: ${_videoEndPos / 1000}ms');
+ // log('UPDATE => START: ${_startPos.dx}, END: ${_endPos.dx}');
+ _scrollStartTimer?.cancel();
+ if (_endPos.dx >= _autoEndScrollPos &&
+ currentScrollValue <= totalVideoLengthInPixels) {
+ startTimer(true);
+ } else if (_startPos.dx <= _autoStartScrollPos &&
+ currentScrollValue != 0.0) {
+ startTimer(false);
+ }
+
+ setState(() {});
+ }
+
+ void _onStartDragged() {
+ if (_scrollingTimer?.isActive ?? false) return;
+ _startFraction = (_startPos.dx / _thumbnailViewerW);
+ _videoStartPos = (_trimmerAreaDuration * _startFraction) +
+ (_scrollController.position.pixels /
+ _scrollController.position.maxScrollExtent) *
+ _remainingDuration;
+ widget.onChangeStart!(_videoStartPos);
+ _linearTween.begin = _startPos.dx;
+ _animationController!.duration =
+ Duration(milliseconds: (_videoEndPos - _videoStartPos).toInt());
+ _animationController!.reset();
+ }
+
+ void _onEndDragged() {
+ if (_scrollingTimer?.isActive ?? false) return;
+ _endFraction = _endPos.dx / _thumbnailViewerW;
+ _videoEndPos = (_trimmerAreaDuration * _endFraction) +
+ (_scrollController.position.pixels /
+ _scrollController.position.maxScrollExtent) *
+ _remainingDuration;
+ widget.onChangeEnd!(_videoEndPos);
+ _linearTween.end = _endPos.dx;
+ _animationController!.duration =
+ Duration(milliseconds: (_videoEndPos - _videoStartPos).toInt());
+ _animationController!.reset();
+ }
+
+ /// Drag gesture ended, update UI accordingly.
+ void _onDragEnd(DragEndDetails details) {
+ log('onDragEnd');
+ _scrollStartTimer?.cancel();
+ _scrollingTimer?.cancel();
+ setState(() {
+ _startCircleSize = widget.editorProperties.circleSize;
+ _endCircleSize = widget.editorProperties.circleSize;
+ if (_dragType == EditorDragType.right) {
+ videoPlayerController
+ .seekTo(Duration(milliseconds: _videoEndPos.toInt()));
+ } else {
+ videoPlayerController
+ .seekTo(Duration(milliseconds: _videoStartPos.toInt()));
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ videoPlayerController.pause();
+ _scrollController.dispose();
+ _scrollStartTimer?.cancel();
+ _scrollingTimer?.cancel();
+ widget.onChangePlaybackState!(false);
+ if (_videoFile != null) {
+ videoPlayerController.setVolume(0.0);
+ videoPlayerController.dispose();
+ widget.onChangePlaybackState!(false);
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onHorizontalDragStart: _onDragStart,
+ onHorizontalDragUpdate: _onDragUpdate,
+ onHorizontalDragEnd: _onDragEnd,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ widget.showDuration
+ ? SizedBox(
+ width: _thumbnailViewerW,
+ child: Padding(
+ padding: const EdgeInsets.only(bottom: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Text(
+ Duration(milliseconds: _videoStartPos.toInt())
+ .toString()
+ .split('.')[0],
+ style: widget.durationTextStyle,
+ ),
+ videoPlayerController.value.isPlaying
+ ? Text(
+ Duration(milliseconds: _currentPosition.toInt())
+ .toString()
+ .split('.')[0],
+ style: widget.durationTextStyle,
+ )
+ : Container(),
+ Text(
+ Duration(milliseconds: _videoEndPos.toInt())
+ .toString()
+ .split('.')[0],
+ style: widget.durationTextStyle,
+ ),
+ ],
+ ),
+ ),
+ )
+ : Container(),
+ Stack(
+ clipBehavior: Clip.none,
+ children: [
+ CustomPaint(
+ foregroundPainter: TrimEditorPainter(
+ startPos: _startPos,
+ endPos: _endPos,
+ scrubberAnimationDx: _scrubberAnimation?.value ?? 0,
+ startCircleSize: _startCircleSize,
+ endCircleSize: _endCircleSize,
+ borderRadius: _borderRadius,
+ borderWidth: widget.editorProperties.borderWidth,
+ scrubberWidth: widget.editorProperties.scrubberWidth,
+ circlePaintColor: widget.editorProperties.circlePaintColor,
+ borderPaintColor: widget.editorProperties.borderPaintColor,
+ scrubberPaintColor:
+ widget.editorProperties.scrubberPaintColor,
+ ),
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ widget.areaProperties.borderRadius),
+ child: Container(
+ key: _trimmerAreaKey,
+ color: Colors.grey[900],
+ height: _thumbnailViewerH,
+ width: _thumbnailViewerW == 0.0
+ ? widget.viewerWidth
+ : _thumbnailViewerW,
+ child: thumbnailWidget ?? Container(),
+ ),
+ ),
+ _scrollController.positions.isNotEmpty
+ ? AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ decoration: BoxDecoration(
+ gradient: widget.areaProperties.blurEdges
+ ? LinearGradient(
+ stops: const [0.0, 0.1, 0.9, 1.0],
+ colors: [
+ _scrollController.position.pixels == 0.0
+ ? Colors.transparent
+ : widget.areaProperties.blurColor,
+ Colors.transparent,
+ Colors.transparent,
+ _scrollController.position.pixels ==
+ _scrollController
+ .position.maxScrollExtent
+ ? Colors.transparent
+ : widget.areaProperties.blurColor,
+ ],
+ )
+ : null,
+ ),
+ height: _thumbnailViewerH,
+ width: widget.viewerWidth,
+ child: Row(
+ children: [
+ AnimatedOpacity(
+ opacity:
+ _scrollController.position.pixels != 0.0
+ ? 1.0
+ : 0.0,
+ duration: const Duration(milliseconds: 300),
+ child: widget.areaProperties.startIcon),
+ const Spacer(),
+ AnimatedOpacity(
+ opacity: _scrollController.position.pixels !=
+ _scrollController
+ .position.maxScrollExtent
+ ? 1.0
+ : 0.0,
+ duration: const Duration(milliseconds: 300),
+ child: widget.areaProperties.endIcon,
+ ),
+ ],
+ ),
+ )
+ : const SizedBox(),
+ ],
+ ),
+ ),
+ // This widget is in development for making the DEBUGGING
+ // process of this package easier
+ Visibility(
+ visible: false,
+ child: Row(
+ children: [
+ Container(
+ color: Colors.red.withOpacity(0.6),
+ height: _thumbnailViewerH,
+ // 2% of total trimmer width
+ width: (_thumbnailViewerW == 0.0
+ ? widget.viewerWidth
+ : _thumbnailViewerW) *
+ 0.02,
+ ),
+ const Spacer(),
+ Container(
+ color: Colors.red.withOpacity(0.6),
+ height: _thumbnailViewerH,
+ // 2% of total trimmer width
+ width: (_thumbnailViewerW == 0.0
+ ? widget.viewerWidth
+ : _thumbnailViewerW) *
+ 0.02,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/trim_viewer/trim_area_properties.dart b/lib/src/trim_viewer/trim_area_properties.dart
new file mode 100644
index 00000000..7c32ca84
--- /dev/null
+++ b/lib/src/trim_viewer/trim_area_properties.dart
@@ -0,0 +1,213 @@
+import 'package:flutter/material.dart';
+
+class TrimAreaProperties {
+ /// For defining the image fit type of each thumbnail image.
+ ///
+ /// By default it is set to `BoxFit.fitHeight`.
+ final BoxFit thumbnailFit;
+
+ /// For specifying the quality of each
+ /// generated image thumbnail, to be displayed in the trimmer
+ /// area.
+ ///
+ /// By default it is set to `75`.
+ final int thumbnailQuality;
+
+ /// For adding a blur to the trim area edges. Use `blurColor`
+ /// for specifying the color of the blur (usually it's the
+ /// background color which helps in blending).
+ ///
+ /// By default it is set to `false`.
+ final bool blurEdges;
+
+ /// For specifying the color of the blur. Use the color of the
+ /// background to blend with it.
+ ///
+ /// By default it is set to `Colors.black`.
+ final Color blurColor;
+
+ /// For specifying the widget to be placed at the start
+ /// of the trimmer area. You can pass `null` for hiding
+ /// the widget.
+ ///
+ /// By default it is set as:
+ ///
+ /// ```dart
+ /// Icon(
+ /// Icons.arrow_back_ios_new_rounded,
+ /// color: Colors.white,
+ /// size: 16,
+ /// )
+ /// ```
+ final Widget? startIcon;
+
+ /// For specifying the widget to be placed at the end
+ /// of the trimmer area. You can pass `null` for hiding
+ /// the widget.
+ ///
+ /// By default it is set as:
+ ///
+ /// ```dart
+ /// Icon(
+ /// Icons.arrow_forward_ios_rounded,
+ /// color: Colors.white,
+ /// size: 16,
+ /// )
+ /// ```
+ final Widget? endIcon;
+
+ /// For specifying the size of the circular border radius
+ /// to be applied to each corner of the trimmer area Container.
+ ///
+ /// By default it is set to `4.0`.
+ final double borderRadius;
+
+ /// Helps defining the Trim Area properties.
+ ///
+ /// A better look at the structure of the **Trim Viewer**:
+ ///
+ /// 
+ ///
+ ///
+ /// All the parameters are optional:
+ ///
+ /// * [thumbnailFit] for specifying the image fit type of each thumbnail image.
+ /// By default it is set to `BoxFit.fitHeight`.
+ ///
+ ///
+ /// * [thumbnailQuality] for specifying the quality of each
+ /// generated image thumbnail, to be displayed in the trimmer
+ /// area. By default it is set to `75`.
+ ///
+ ///
+ /// * [blurEdges] for adding a blur to the trim area edges. Use `blurColor`
+ /// for specifying the color of the blur (usually it's the background color
+ /// which helps in blending). By default it is set to `false`.
+ ///
+ ///
+ /// * [blurColor] for specifying the color of the blur. Use the color of the
+ /// background to blend with it. By default it is set to `Colors.black`.
+ ///
+ ///
+ /// * [startIcon] for specifying the widget to be placed at the start
+ /// of the trimmer area. You can pass `null` for hiding
+ /// the widget.
+ ///
+ ///
+ /// * [endIcon] for specifying the widget to be placed at the end
+ /// of the trimmer area. You can pass `null` for hiding
+ /// the widget.
+ ///
+ ///
+ /// * [borderRadius] for specifying the size of the circular border radius
+ /// to be applied to each corner of the trimmer area Container.
+ /// By default it is set to `4.0`.
+ ///
+ const TrimAreaProperties({
+ this.thumbnailFit = BoxFit.fitHeight,
+ this.thumbnailQuality = 75,
+ this.blurEdges = false,
+ this.blurColor = Colors.black,
+ this.startIcon,
+ this.endIcon,
+ this.borderRadius = 4.0,
+ });
+
+ /// Helps defining the Fixed Trim Area properties.
+ ///
+ /// A better look at the structure of the **Trim Viewer**:
+ ///
+ /// 
+ ///
+ ///
+ /// All the parameters are optional:
+ ///
+ /// * [thumbnailFit] for specifying the image fit type of each thumbnail image.
+ /// By default it is set to `BoxFit.fitHeight`.
+ ///
+ ///
+ /// * [thumbnailQuality] for specifying the quality of each
+ /// generated image thumbnail, to be displayed in the trimmer
+ /// area. By default it is set to `75`.
+ ///
+ ///
+ /// * [borderRadius] for specifying the size of the circular border radius
+ /// to be applied to each corner of the trimmer area Container.
+ /// By default it is set to `4.0`.
+ ///
+ factory TrimAreaProperties.fixed({
+ BoxFit thumbnailFit,
+ int thumbnailQuality,
+ double borderRadius,
+ }) = FixedTrimAreaProperties;
+
+ /// Helps defining the Trim Area properties with blur & arrows on the edges.
+ ///
+ /// A better look at the structure of the **Trim Viewer**:
+ ///
+ /// 
+ ///
+ factory TrimAreaProperties.edgeBlur({
+ BoxFit thumbnailFit,
+ int thumbnailQuality,
+ bool blurEdges,
+ Color blurColor,
+ Widget? startIcon,
+ Widget? endIcon,
+ double borderRadius,
+ }) = _TrimAreaPropertiesWithBlur;
+}
+
+class FixedTrimAreaProperties extends TrimAreaProperties {
+ /// Helps defining the Fixed Trim Area properties.
+ ///
+ /// A better look at the structure of the **Trim Viewer**:
+ ///
+ /// 
+ ///
+ ///
+ /// All the parameters are optional:
+ ///
+ /// * [thumbnailFit] for specifying the image fit type of each thumbnail image.
+ /// By default it is set to `BoxFit.fitHeight`.
+ ///
+ ///
+ /// * [thumbnailQuality] for specifying the quality of each
+ /// generated image thumbnail, to be displayed in the trimmer
+ /// area. By default it is set to `75`.
+ ///
+ ///
+ /// * [borderRadius] for specifying the size of the circular border radius
+ /// to be applied to each corner of the trimmer area Container.
+ /// By default it is set to `4.0`.
+ ///
+ const FixedTrimAreaProperties({
+ super.thumbnailFit,
+ super.thumbnailQuality,
+ super.borderRadius,
+ });
+}
+
+class _TrimAreaPropertiesWithBlur extends TrimAreaProperties {
+ _TrimAreaPropertiesWithBlur({
+ super.thumbnailFit,
+ super.thumbnailQuality,
+ blurEdges,
+ super.blurColor,
+ super.borderRadius,
+ endIcon,
+ startIcon,
+ }) : super(
+ blurEdges: true,
+ startIcon: const Icon(
+ Icons.arrow_back_ios_new_rounded,
+ color: Colors.white,
+ size: 16,
+ ),
+ endIcon: const Icon(
+ Icons.arrow_forward_ios_rounded,
+ color: Colors.white,
+ size: 16,
+ ),
+ );
+}
diff --git a/lib/src/trim_editor_painter.dart b/lib/src/trim_viewer/trim_editor_painter.dart
similarity index 74%
rename from lib/src/trim_editor_painter.dart
rename to lib/src/trim_viewer/trim_editor_painter.dart
index 38202dc4..9e7ab84a 100644
--- a/lib/src/trim_editor_painter.dart
+++ b/lib/src/trim_viewer/trim_editor_painter.dart
@@ -10,10 +10,20 @@ class TrimEditorPainter extends CustomPainter {
/// To define the horizontal length of the selected video area
final double scrubberAnimationDx;
- /// For specifying a size to the holder at the
- /// two ends of the video trimmer area, while it is `idle`.
+ /// For specifying a circular border radius
+ /// to the corners of the trim area.
+ /// By default it is set to `4.0`.
+ final double borderRadius;
+
+ /// For specifying a size to the start holder
+ /// of the video trimmer area.
+ /// By default it is set to `0.5`.
+ final double startCircleSize;
+
+ /// For specifying a size to the end holder
+ /// of the video trimmer area.
/// By default it is set to `0.5`.
- final double circleSize;
+ final double endCircleSize;
/// For specifying the width of the border around
/// the trim area. By default it is set to `3`.
@@ -55,11 +65,21 @@ class TrimEditorPainter extends CustomPainter {
///
/// The optional parameters are:
///
- /// * [circleSize] for specifying a size to the holder at the
- /// two ends of the video trimmer area, while it is `idle`.
+ /// * [startCircleSize] for specifying a size to the start holder
+ /// of the video trimmer area.
+ /// By default it is set to `0.5`.
+ ///
+ ///
+ /// * [endCircleSize] for specifying a size to the end holder
+ /// of the video trimmer area.
/// By default it is set to `0.5`.
///
///
+ /// * [borderRadius] for specifying a circular border radius
+ /// to the corners of the trim area.
+ /// By default it is set to `4.0`.
+ ///
+ ///
/// * [borderWidth] for specifying the width of the border around
/// the trim area. By default it is set to `3`.
///
@@ -86,7 +106,9 @@ class TrimEditorPainter extends CustomPainter {
required this.startPos,
required this.endPos,
required this.scrubberAnimationDx,
- this.circleSize = 0.5,
+ this.startCircleSize = 0.5,
+ this.endCircleSize = 0.5,
+ this.borderRadius = 4,
this.borderWidth = 3,
this.scrubberWidth = 1,
this.showScrubber = true,
@@ -116,6 +138,10 @@ class TrimEditorPainter extends CustomPainter {
..strokeCap = StrokeCap.round;
final rect = Rect.fromPoints(startPos, endPos);
+ final roundedRect = RRect.fromRectAndRadius(
+ rect,
+ Radius.circular(borderRadius),
+ );
if (showScrubber) {
if (scrubberAnimationDx.toInt() > startPos.dx.toInt()) {
@@ -127,11 +153,13 @@ class TrimEditorPainter extends CustomPainter {
}
}
- canvas.drawRect(rect, borderPaint);
+ canvas.drawRRect(roundedRect, borderPaint);
+ // Paint start holder
canvas.drawCircle(
- startPos + Offset(0, endPos.dy / 2), circleSize, circlePaint);
+ startPos + Offset(0, endPos.dy / 2), startCircleSize, circlePaint);
+ // Paint end holder
canvas.drawCircle(
- endPos + Offset(0, -endPos.dy / 2), circleSize, circlePaint);
+ endPos + Offset(0, -endPos.dy / 2), endCircleSize, circlePaint);
}
@override
diff --git a/lib/src/trim_viewer/trim_editor_properties.dart b/lib/src/trim_viewer/trim_editor_properties.dart
new file mode 100644
index 00000000..17fff809
--- /dev/null
+++ b/lib/src/trim_viewer/trim_editor_properties.dart
@@ -0,0 +1,110 @@
+import 'package:flutter/material.dart';
+
+class TrimEditorProperties {
+ /// For specifying a size to the holder at the
+ /// two ends of the video trimmer area, while it is `idle`.
+ ///
+ /// By default it is set to `5.0`.
+ final double circleSize;
+
+ /// For specifying a size to the holder at
+ /// the two ends of the video trimmer area, while it is being
+ /// `dragged`.
+ ///
+ /// By default it is set to `8.0`.
+ final double circleSizeOnDrag;
+
+ /// For specifying the width of the border around
+ /// the trim area. By default it is set to `3`.
+ final double borderWidth;
+
+ /// For specifying the width of the video scrubber
+ final double scrubberWidth;
+
+ /// For specifying a circular border radius
+ /// to the corners of the trim area.
+ ///
+ /// By default it is set to `4.0`.
+ final double borderRadius;
+
+ /// For specifying a color to the circle.
+ ///
+ /// By default it is set to `Colors.white`.
+ final Color circlePaintColor;
+
+ /// For specifying a color to the border of
+ /// the trim area.
+ ///
+ /// By default it is set to `Colors.white`.
+ final Color borderPaintColor;
+
+ /// For specifying a color to the video
+ /// scrubber inside the trim area.
+ ///
+ /// By default it is set to `Colors.white`.
+ final Color scrubberPaintColor;
+
+ /// Determines the touch size of the side handles, left and right. The rest, in
+ /// the center, will move the whole frame if [maxVideoLength] is inferior to the
+ /// total duration of the video.
+ final int sideTapSize;
+
+ /// Helps defining the Trim Editor properties.
+ ///
+ /// A better look at the structure of the **Trim Viewer**:
+ ///
+ /// 
+ ///
+ ///
+ /// All the parameters are optional:
+ ///
+ /// * [circleSize] for specifying a size to the holder at the
+ /// two ends of the video trimmer area, while it is `idle`.
+ /// By default it is set to `5.0`.
+ ///
+ ///
+ /// * [circleSizeOnDrag] for specifying a size to the holder at
+ /// the two ends of the video trimmer area, while it is being
+ /// `dragged`. By default it is set to `8.0`.
+ ///
+ ///
+ /// * [borderWidth] for specifying the width of the border around
+ /// the trim area. By default it is set to `3.0`.
+ ///
+ /// * [scrubberWidth] for specifying the width of the video scrubber.
+ /// By default it is set to `1.0`.
+ ///
+ ///
+ /// * [borderRadius] for applying a circular border radius
+ /// to the corners of the trim area. By default it is set to `4.0`.
+ ///
+ ///
+ /// * [circlePaintColor] for specifying a color to the circle.
+ /// By default it is set to `Colors.white`.
+ ///
+ ///
+ /// * [borderPaintColor] for specifying a color to the border of
+ /// the trim area. By default it is set to `Colors.white`.
+ ///
+ ///
+ /// * [scrubberPaintColor] for specifying a color to the video
+ /// scrubber inside the trim area. By default it is set to
+ /// `Colors.white`.
+ ///
+ ///
+ /// * [sideTapSize] determines the touch size of the side handles, left and right.
+ /// The rest, in the center, will move the whole frame if [maxVideoLength] is
+ /// inferior to the total duration of the video.
+ ///
+ const TrimEditorProperties({
+ this.circleSize = 5.0,
+ this.circleSizeOnDrag = 8.0,
+ this.borderWidth = 3.0,
+ this.scrubberWidth = 1.0,
+ this.borderRadius = 4.0,
+ this.circlePaintColor = Colors.white,
+ this.borderPaintColor = Colors.white,
+ this.scrubberPaintColor = Colors.white,
+ this.sideTapSize = 24,
+ });
+}
diff --git a/lib/src/trim_viewer/trim_viewer.dart b/lib/src/trim_viewer/trim_viewer.dart
new file mode 100644
index 00000000..50bf548e
--- /dev/null
+++ b/lib/src/trim_viewer/trim_viewer.dart
@@ -0,0 +1,270 @@
+import 'package:flutter/material.dart';
+import 'package:video_trimmer/video_trimmer.dart';
+
+import 'fixed_viewer/fixed_trim_viewer.dart';
+import 'scrollable_viewer/scrollable_trim_viewer.dart';
+
+enum ViewerType {
+ /// Automatically decide whether to use the
+ /// fixed length or scrollable editor.
+ auto,
+
+ /// Use fixed length editor, `FixedTrimViewer`.
+ fixed,
+
+ /// Use scrollable editor, `ScrollableTrimViewer`.
+ scrollable,
+}
+
+class TrimViewer extends StatefulWidget {
+ /// The Trimmer instance controlling the data.
+ final Trimmer trimmer;
+
+ /// For defining the total trimmer area width
+ final double viewerWidth;
+
+ /// For defining the total trimmer area height
+ final double viewerHeight;
+
+ /// For specifying the type of the trim viewer.
+ /// You can choose among: `auto`, `fixed`, and `scrollable`.
+ ///
+ /// **NOTE:** While using `scrollable` if the total video
+ /// duration is less than maxVideoLength + padding, it
+ /// will throw an error.
+ ///
+ /// By default it is set to `ViewerType.auto`.
+ final ViewerType type;
+
+ /// For defining the maximum length of the output video.
+ ///
+ /// **NOTE:** When explicitly setting the `type` to `scrollable`,
+ /// specifying this property is mandatory.
+ final Duration maxVideoLength;
+
+ /// For showing the start and the end point of the
+ /// video on top of the trimmer area.
+ ///
+ /// By default it is set to `true`.
+ final bool showDuration;
+
+ /// For providing a `TextStyle` to the
+ /// duration text.
+ ///
+ /// By default it is set to `TextStyle(color: Colors.white)`
+ final TextStyle durationTextStyle;
+
+ /// Callback to the video start position
+ ///
+ /// Returns the selected video start position in `milliseconds`.
+ final Function(double startValue)? onChangeStart;
+
+ /// Callback to the video end position.
+ ///
+ /// Returns the selected video end position in `milliseconds`.
+ final Function(double endValue)? onChangeEnd;
+
+ /// Callback to the video playback
+ /// state to know whether it is currently playing or paused.
+ ///
+ /// Returns a `boolean` value. If `true`, video is currently
+ /// playing, otherwise paused.
+ final Function(bool isPlaying)? onChangePlaybackState;
+
+ /// This is the fraction of padding present beside the trimmer editor,
+ /// calculated on the `maxVideoLength` value.
+ final double paddingFraction;
+
+ /// Properties for customizing the trim editor.
+ final TrimEditorProperties editorProperties;
+
+ /// Properties for customizing the trim area.
+ final TrimAreaProperties areaProperties;
+
+ /// Callback for thumbnail loader to know when all the
+ /// thumbnails are loaded.
+ final VoidCallback? onThumbnailLoadingComplete;
+
+ /// Widget for displaying the video trimmer.
+ ///
+ /// This has frame wise preview of the video with a
+ /// slider for selecting the part of the video to be
+ /// trimmed. It automatically selected whether to use
+ /// `FixedTrimViewer` or `ScrollableTrimViewer`.
+ ///
+ /// If you want to use a specific kind of trim viewer, use
+ /// the `type` property.
+ ///
+ /// The required parameters are [viewerWidth] & [viewerHeight]
+ ///
+ /// * [viewerWidth] to define the total trimmer area width.
+ ///
+ ///
+ /// * [viewerHeight] to define the total trimmer area height.
+ ///
+ ///
+ /// The optional parameters are:
+ ///
+ /// * [type] for specifying the type of the trim viewer.
+ ///
+ ///
+ /// * [fit] for specifying the image fit type of each thumbnail image.
+ /// By default it is set to `BoxFit.fitHeight`.
+ ///
+ ///
+ /// * [maxVideoLength] for specifying the maximum length of the
+ /// output video.
+ ///
+ ///
+ /// * [circlePaintColor] for specifying a color to the circle.
+ /// By default it is set to `Colors.white`.
+ ///
+ ///
+ /// * [borderPaintColor] for specifying a color to the border of
+ /// the trim area. By default it is set to `Colors.white`.
+ ///
+ ///
+ /// * [scrubberPaintColor] for specifying a color to the video
+ /// scrubber inside the trim area. By default it is set to
+ /// `Colors.white`.
+ ///
+ ///
+ /// * [thumbnailQuality] for specifying the quality of each
+ /// generated image thumbnail, to be displayed in the trimmer
+ /// area.
+ ///
+ ///
+ /// * [showDuration] for showing the start and the end point of the
+ /// video on top of the trimmer area. By default it is set to `true`.
+ ///
+ ///
+ /// * [durationTextStyle] is for providing a `TextStyle` to the
+ /// duration text. By default it is set to
+ /// `TextStyle(color: Colors.white)`
+ ///
+ ///
+ /// * [onChangeStart] is a callback to the video start position.
+ ///
+ ///
+ /// * [onChangeEnd] is a callback to the video end position.
+ ///
+ ///
+ /// * [onChangePlaybackState] is a callback to the video playback
+ /// state to know whether it is currently playing or paused.
+ ///
+ ///
+ /// * [editorProperties] defines properties for customizing the trim editor.
+ ///
+ ///
+ /// * [areaProperties] defines properties for customizing the trim area.
+ ///
+ ///
+ /// * [onThumbnailLoadingComplete] is a callback for thumbnail loader to
+ /// know when all the thumbnails are loaded.
+ ///
+ const TrimViewer({
+ Key? key,
+ required this.trimmer,
+ this.maxVideoLength = const Duration(milliseconds: 0),
+ this.type = ViewerType.auto,
+ this.viewerWidth = 50 * 8,
+ this.viewerHeight = 50,
+ this.showDuration = true,
+ this.durationTextStyle = const TextStyle(color: Colors.white),
+ this.onChangeStart,
+ this.onChangeEnd,
+ this.onChangePlaybackState,
+ this.paddingFraction = 0.2,
+ this.editorProperties = const TrimEditorProperties(),
+ this.areaProperties = const TrimAreaProperties(),
+ this.onThumbnailLoadingComplete,
+ }) : super(key: key);
+
+ @override
+ State createState() => _TrimViewerState();
+}
+
+class _TrimViewerState extends State with TickerProviderStateMixin {
+ bool? _isScrollableAllowed;
+
+ @override
+ void initState() {
+ super.initState();
+ widget.trimmer.eventStream.listen((event) {
+ if (event == TrimmerEvent.initialized) {
+ final totalDuration =
+ widget.trimmer.videoPlayerController!.value.duration;
+ final maxVideoLength = widget.maxVideoLength;
+ final paddingFraction = widget.paddingFraction;
+ final trimAreaDuration = Duration(
+ milliseconds: (maxVideoLength.inMilliseconds +
+ ((paddingFraction * maxVideoLength.inMilliseconds) * 2)
+ .toInt()));
+
+ final shouldScroll = trimAreaDuration <= totalDuration &&
+ maxVideoLength.compareTo(const Duration(milliseconds: 0)) != 0;
+ if (widget.type == ViewerType.scrollable && !shouldScroll) {
+ throw 'Total video duration is less than maxVideoLength + padding. '
+ 'Can\'t use `ScrollableTrimViewer`. Change the type to `ViewerType.auto`.';
+ }
+ setState(() => _isScrollableAllowed = shouldScroll);
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final scrollableViewer = ScrollableTrimViewer(
+ trimmer: widget.trimmer,
+ maxVideoLength: widget.maxVideoLength,
+ viewerWidth: widget.viewerWidth,
+ viewerHeight: widget.viewerHeight,
+ showDuration: widget.showDuration,
+ durationTextStyle: widget.durationTextStyle,
+ onChangeStart: widget.onChangeStart,
+ onChangeEnd: widget.onChangeEnd,
+ onChangePlaybackState: widget.onChangePlaybackState,
+ paddingFraction: widget.paddingFraction,
+ editorProperties: widget.editorProperties,
+ areaProperties: widget.areaProperties,
+ onThumbnailLoadingComplete: () {
+ if (widget.onThumbnailLoadingComplete != null) {
+ widget.onThumbnailLoadingComplete!();
+ }
+ },
+ );
+
+ final fixedTrimViewer = FixedTrimViewer(
+ trimmer: widget.trimmer,
+ maxVideoLength: widget.maxVideoLength,
+ viewerWidth: widget.viewerWidth,
+ viewerHeight: widget.viewerHeight,
+ showDuration: widget.showDuration,
+ durationTextStyle: widget.durationTextStyle,
+ onChangeStart: widget.onChangeStart,
+ onChangeEnd: widget.onChangeEnd,
+ onChangePlaybackState: widget.onChangePlaybackState,
+ editorProperties: widget.editorProperties,
+ areaProperties: FixedTrimAreaProperties(
+ thumbnailFit: widget.areaProperties.thumbnailFit,
+ thumbnailQuality: widget.areaProperties.thumbnailQuality,
+ borderRadius: widget.areaProperties.borderRadius,
+ ),
+ onThumbnailLoadingComplete: () {
+ if (widget.onThumbnailLoadingComplete != null) {
+ widget.onThumbnailLoadingComplete!();
+ }
+ },
+ );
+
+ return _isScrollableAllowed == null
+ ? const SizedBox()
+ : widget.type == ViewerType.fixed
+ ? fixedTrimViewer
+ : widget.type == ViewerType.scrollable
+ ? scrollableViewer
+ : _isScrollableAllowed == true
+ ? scrollableViewer
+ : fixedTrimViewer;
+ }
+}
diff --git a/lib/src/trimmer.dart b/lib/src/trimmer.dart
index dab171e4..81ddd22f 100644
--- a/lib/src/trimmer.dart
+++ b/lib/src/trimmer.dart
@@ -9,8 +9,8 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';
-import 'package:video_trimmer/src/file_formats.dart';
-import 'package:video_trimmer/src/storage_dir.dart';
+import 'package:video_trimmer/src/utils/file_formats.dart';
+import 'package:video_trimmer/src/utils/storage_dir.dart';
enum TrimmerEvent { initialized }
diff --git a/lib/src/utils/editor_drag_type.dart b/lib/src/utils/editor_drag_type.dart
new file mode 100644
index 00000000..ce1aae57
--- /dev/null
+++ b/lib/src/utils/editor_drag_type.dart
@@ -0,0 +1,10 @@
+enum EditorDragType {
+ /// The user is dragging the left part of the frame.
+ left,
+
+ /// The user is dragging the whole frame.
+ center,
+
+ /// The user is dragging the right part of the frame.
+ right
+}
diff --git a/lib/src/file_formats.dart b/lib/src/utils/file_formats.dart
similarity index 100%
rename from lib/src/file_formats.dart
rename to lib/src/utils/file_formats.dart
diff --git a/lib/src/storage_dir.dart b/lib/src/utils/storage_dir.dart
similarity index 100%
rename from lib/src/storage_dir.dart
rename to lib/src/utils/storage_dir.dart
diff --git a/lib/video_trimmer.dart b/lib/video_trimmer.dart
index ce8133b5..c6942c75 100644
--- a/lib/video_trimmer.dart
+++ b/lib/video_trimmer.dart
@@ -1,9 +1,11 @@
library video_trimmer;
-export 'package:video_trimmer/src/video_viewer.dart';
export 'package:video_trimmer/src/trimmer.dart';
-export 'package:video_trimmer/src/trim_editor.dart';
-export 'package:video_trimmer/src/trim_editor_painter.dart';
-export 'package:video_trimmer/src/thumbnail_viewer.dart';
-export 'package:video_trimmer/src/file_formats.dart';
-export 'package:video_trimmer/src/storage_dir.dart';
+export 'package:video_trimmer/src/video_viewer.dart';
+export 'package:video_trimmer/src/utils/file_formats.dart';
+export 'package:video_trimmer/src/utils/storage_dir.dart';
+export 'package:video_trimmer/src/trim_viewer/trim_editor_properties.dart';
+export 'package:video_trimmer/src/trim_viewer/trim_area_properties.dart';
+// Two types of trim viewers, the `TrimViewer` class helps to auto select
+// based on the length.
+export 'package:video_trimmer/src/trim_viewer/trim_viewer.dart';
diff --git a/pubspec.lock b/pubspec.lock
index e7ef97ed..b920ed2b 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -212,6 +212,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
+ transparent_image:
+ dependency: "direct main"
+ description:
+ name: transparent_image
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
vector_math:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 229e0dd6..e7997246 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: video_trimmer
description: A Flutter package for trimming videos. This supports retrieving, trimming, and storage of trimmed video files to the file system.
-version: 1.2.0
+version: 2.0.0
homepage: https://github.com/sbis04/video_trimmer
environment:
@@ -16,6 +16,7 @@ dependencies:
path_provider: ^2.0.11
intl: ^0.17.0
path: ^1.8.2
+ transparent_image: ^2.0.0
dev_dependencies:
flutter_lints: ^2.0.1
diff --git a/screenshots/trim_viewer_preview.png b/screenshots/trim_viewer_preview.png
new file mode 100644
index 00000000..bbc1d300
Binary files /dev/null and b/screenshots/trim_viewer_preview.png differ
diff --git a/screenshots/trim_viewer_preview_small.png b/screenshots/trim_viewer_preview_small.png
new file mode 100644
index 00000000..a29c0bd4
Binary files /dev/null and b/screenshots/trim_viewer_preview_small.png differ
diff --git a/screenshots/updated_trimmer_demo.gif b/screenshots/updated_trimmer_demo.gif
new file mode 100644
index 00000000..f96dc1eb
Binary files /dev/null and b/screenshots/updated_trimmer_demo.gif differ