From 1bb91e35ed92b6fcb162ecd4de7fb34f08ba6b17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ma=C3=ABl?=
Date: Sun, 14 Jul 2024 21:19:19 +0900
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Video=20support=20(#50)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ♻️ add basic video player in crop_viewer
* ✨ show videos fake cropped in result view
* ♻️ update video widgets
* ♻️ show video in crop view
* 🐛 fix always hidden crop ratio button
* ✨ show crop video preview
* 🐛 fix `InstaAssetCropTransform`
* 🐛 fix `InstaAssetCropTransform` to fit aspect ratio
* 💄 fix crop loader
* 💄 fix video thumbnail size
* ♻️ export photo_manger
* 🚨 remove import
* 💄 improve placeholders and image loading/error
* 💄 fix cropview loading & error widgets
* ✨ add `skipCropOnComplete` options builder param
* 🐛 fix camera example crop view size issue
* ♻️ update CropResult page in example
* ✨ add `previewThumbnailSize` config parameter
* ♻️ update camera examples to supports video
* 🐛 fix camera example asset from picture broken size
* ➕ use git dependency
* ♻️ move cropDelegate into InstaAssetPickerConfig
* ✨ new `InstaAssetsExportData` class
* ✨ add `ffmpegCrop` and `ffmpegScale`, add ffmpeg in example
* ♻️ improve example export progress with ffmpeg statistics
* 🐛 fix camera video_player exception
* 🐛 fix iCloud video loading
* 💄 fix iCloud loading size
* 🐛 fix saving crop param on dispose
* 🐛 fix camera examples
* 📝 update doc
* ⬆️ use latest insta_assets_crop
* 📝 update changelog, readme and migration guide
* 📝 update doc
* 📝 update doc
* 📝 update doc
* 📝 update doc
---
CHANGELOG.md | 19 +
MIGRATION_GUIDE.md | 46 +++
README.md | 110 +++---
example/analysis_options.yaml | 4 +-
example/android/app/build.gradle | 5 +-
.../android/app/src/main/AndroidManifest.xml | 3 +
example/ios/Podfile | 2 +-
example/ios/Podfile.lock | 18 +-
example/lib/main.dart | 31 +-
example/lib/pages/camera/camera_picker.dart | 272 ++++++++++---
.../pages/camera/wechat_camera_picker.dart | 38 +-
example/lib/pages/restorable_picker.dart | 13 +-
example/lib/pages/stateless_pickers.dart | 6 +-
example/lib/post_provider.dart | 171 ++++++++
example/lib/utils.dart | 14 +
example/lib/widgets/crop_result_view.dart | 157 ++++++--
.../lib/widgets/insta_picker_interface.dart | 10 +
example/lib/widgets/post.dart | 217 ++++++++++
example/pubspec.lock | 30 +-
example/pubspec.yaml | 5 +-
lib/insta_assets_picker.dart | 15 +-
lib/src/assets_picker.dart | 66 ++--
lib/src/insta_assets_crop_controller.dart | 110 ++++--
lib/src/widget/crop_transform.dart | 64 +++
lib/src/widget/crop_viewer.dart | 371 ++++++++++++------
.../widget/insta_asset_picker_delegate.dart | 18 +-
lib/src/widget/video_player_mixin.dart | 163 ++++++++
pubspec.yaml | 14 +-
28 files changed, 1628 insertions(+), 364 deletions(-)
create mode 100644 MIGRATION_GUIDE.md
create mode 100644 example/lib/post_provider.dart
create mode 100644 example/lib/utils.dart
create mode 100644 example/lib/widgets/post.dart
create mode 100644 lib/src/widget/crop_transform.dart
create mode 100644 lib/src/widget/video_player_mixin.dart
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ab95fc..7ae2a29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,22 @@
+# Changelog
+
+## 3.0.0-dev.1
+
+### Features
+
+- Video support [#50](https://github.com/LeGoffMael/insta_assets_picker/pull/50)
+ - video processing must be handled manually
+ - new `requestType` param set to `RequestType.common` by default.
+ - new `previewThumbnailSize` & `skipCropOnComplete` config parameters.
+ - new `InstaAssetCropTransform` widget to preview the cropped asset.
+- Crop view initialization time is now much faster.
+
+### [Breaking changes](MIGRATION_GUIDE.md#3.0.0-dev.1)
+
+- new `InstaAssetPickerConfig` config class to provide picker configuration [#48](https://github.com/LeGoffMael/insta_assets_picker/pull/48)
+ - new `gridThumbnailSize`, `themeColor` & `selectPredicate` parameters
+- updated `InstaAssetsExportDetails` class, crop file are now nullable and all the crop parameters are provided in a new class called `InstaAssetsExportData`.
+
## 2.3.1
- bump `wechat_assets_picker` to 9.1.0
diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
new file mode 100644
index 0000000..6835f06
--- /dev/null
+++ b/MIGRATION_GUIDE.md
@@ -0,0 +1,46 @@
+# Migration Guide
+
+This document gathered all breaking changes and migrations requirement between major versions.
+
+## 3.0.0-dev.1
+
+### InstaAssetPickerConfig
+
+The picker configuration parameters must not be provided into a `InstaAssetPickerConfig` class in `pickerConfig`
+
+```diff
+InstaAssetPicker.pickAssets(
+ context,
+- title: 'Example title',
+- pickerTheme: widget.getPickerTheme(context),
++ pickerConfig: InstaAssetPickerConfig(
++ title: 'Example title',
++ pickerTheme: widget.getPickerTheme(context),
++ ),
+)
+```
+
+The picker is now showing image and video assets by default. To show only images, you can change the `requestType` param.
+```diff
+InstaAssetPicker.pickAssets(
+ context,
++ requestType: RequestType.image
+)
+```
+
+
+The `InstaAssetsExportDetails` was also updated.
+The cropped files are now nullable, an all the crop parameters are returned in a new class called `InstaAssetsExportData`.
+
+```diff
++ class InstaAssetsExportData {
++ final File? croppedFile;
++ final InstaAssetsCropData selectedData;
++ }
+
+InstaAssetsExportDetails {
+- final List croppedFiles;
++ final List data;
+```
+
+
diff --git a/README.md b/README.md
index c354b14..8673690 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-An image picker based on Instagram picker UI. It is using the powerful [flutter_wechat_assets_picker](https://pub.dev/packages/wechat_assets_picker)
+An image (also with videos) picker based on Instagram picker UI. It is using the powerful [flutter_wechat_assets_picker](https://pub.dev/packages/wechat_assets_picker)
package to handle the picker and a custom version of [image_crop](https://pub.dev/packages/image_crop) for crop.
## 🚀 Features
@@ -23,15 +23,15 @@ package to handle the picker and a custom version of [image_crop](https://pub.de
- ✅ Instagram layout
- Scroll behaviors, animation
- Preview, select, unselect action logic
+- ✅ Image and Video ([but not video processing](#video)) support
- ✅ Theme and language customization
-- ✅ Multiple images pick (with maximum limit)
-- ✅ Single image pick mode
+- ✅ Multiple assets pick (with maximum limit)
+- ✅ Single asset pick mode
- ✅ Restore state of picker after pop
-- ✅ Select aspect ratios to crop all images with (default to 1:1 & 4:5)
-- ✅ Crop all images at once and receive a stream with a progress value
+- ✅ Select aspect ratios to crop all assets with (default to 1:1 & 4:5)
+- ✅ Crop all image assets at once and receive a stream with a progress value
- ✅ Prepend or append a custom item in the assets list
- ✅ Add custom action buttons
-- ❌ Videos are not supported
## 📸 Screenshots
@@ -58,7 +58,9 @@ For more details check out the [example](https://github.com/LeGoffMael/insta_ass
```dart
Future?> callPicker() => InstaAssetPicker.pickAssets(
context,
- title: 'Select images',
+ pickerConfig: InstaAssetPickerConfig(
+ title: 'Select assets',
+ ),
maxAssets: 10,
onCompleted: (Stream stream) {
// TODO : handle crop stream result
@@ -73,13 +75,19 @@ Future?> callPicker() => InstaAssetPicker.pickAssets(
Fields in `InstaAssetsExportDetails`:
-| Name | Type | Description |
-| -------------- | ------------------- | ------------------------------------------------------- |
-| croppedFiles | `List` | List of all cropped files |
-| selectedAssets | `List` | Selected assets without crop |
-| aspectRatio | `double` | Selected aspect ratio (1 or 4/5) |
-| progress | `double` | Progress indicator of the exportation (between 0 and 1) |
+| Name | Type | Description |
+| -------------- | ----------------------------- | --------------------------------------------------------------------- |
+| data | `List` | Contains the selected assets, crop parameters and possible crop file. |
+| selectedAssets | `List` | Selected assets without crop |
+| aspectRatio | `double` | Selected aspect ratio (1 or 4/5) |
+| progress | `double` | Progress indicator of the exportation (between 0 and 1) |
+
+Fields in `InstaAssetsExportData`:
+| Name | Type | Description |
+| ------------ | --------------------- | ------------------------------------------------------------------ |
+| croppedFile | `File?` | The cropped file. Can be null if video or if choose to skip crop. |
+| selectedData | `InstaAssetsCropData` | The selected asset and it's crop parameter (area, scale, ratio...) |
### Picker configuration
@@ -98,24 +106,26 @@ Most of the components of the picker can be customized using theme.
final theme = InstaAssetPicker.themeData(Theme.of(context).primaryColor);
InstaAssetPicker.pickAssets(
context,
- pickerTheme: theme.copyWith(
- canvasColor: Colors.black, // body background color
- splashColor: Color.grey, // ontap splash color
- colorScheme: theme.colorScheme.copyWith(
- background: Colors.black87, // albums list background color
- ),
- appBarTheme: theme.appBarTheme.copyWith(
- backgroundColor: Colors.black, // app bar background color
- titleTextStyle: Theme.of(context)
- .appBarTheme
- .titleTextStyle
- ?.copyWith(color: Colors.white), // change app bar title text style to be like app theme
- ),
- // edit `confirm` button style
- textButtonTheme: TextButtonThemeData(
- style: TextButton.styleFrom(
- foregroundColor: Colors.blue,
- disabledForegroundColor: Colors.red,
+ pickerConfig: InstaAssetPickerConfig(
+ pickerTheme: theme.copyWith(
+ canvasColor: Colors.black, // body background color
+ splashColor: Color.grey, // ontap splash color
+ colorScheme: theme.colorScheme.copyWith(
+ background: Colors.black87, // albums list background color
+ ),
+ appBarTheme: theme.appBarTheme.copyWith(
+ backgroundColor: Colors.black, // app bar background color
+ titleTextStyle: Theme.of(context)
+ .appBarTheme
+ .titleTextStyle
+ ?.copyWith(color: Colors.white), // change app bar title text style to be like app theme
+ ),
+ // edit `confirm` button style
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.blue,
+ disabledForegroundColor: Colors.red,
+ ),
),
),
),
@@ -126,24 +136,26 @@ InstaAssetPicker.pickAssets(
### Crop customization
You can set the list of crop aspect ratios available.
-You can also set the preferred size, for the cropped images.
+You can also set the preferred size, for the cropped assets.
```dart
InstaAssetPicker.pickAssets(
context,
- cropDelegate: InstaAssetCropDelegate(
- // allows you to set the preferred size used when cropping the image.
- // the final size will depends on the scale used when cropping.
- preferredSize: 1080,
- cropRatios: [
- // - allow you to set the list of aspect ratios selectable,
- // the default values are [1/1, 4/5] like instagram.
- // - if you want to disable cropping, you can set only one parameter,
- // in this case, the "crop" button will not be displayed (#10).
- // - if the value of cropRatios is different than the default value,
- // the "crop" button will display the selected ratio value (i.e.: 1:1)
- // instead of unfold arrows.
- ]),
+ pickerConfig: InstaAssetPickerConfig(
+ cropDelegate: InstaAssetCropDelegate(
+ // allows you to set the preferred size used when cropping the asset.
+ // the final size will depends on the scale used when cropping.
+ preferredSize: 1080,
+ cropRatios: [
+ // - allow you to set the list of aspect ratios selectable,
+ // the default values are [1/1, 4/5] like instagram.
+ // - if you want to disable cropping, you can set only one parameter,
+ // in this case, the "crop" button will not be displayed (#10).
+ // - if the value of cropRatios is different than the default value,
+ // the "crop" button will display the selected ratio value (i.e.: 1:1)
+ // instead of unfold arrows.
+ ]),
+ ),
onCompleted: (_) {},
);
```
@@ -158,6 +170,14 @@ However, since version `2.0.0`, it is now possible to trigger this action using
The ability to take a photo from the camera must be handled on your side, but the picker is now able to refresh the list and select the new photo.
New [examples](https://github.com/LeGoffMael/insta_assets_picker/tree/main/example/lib/pages/camera) have been written to show how to manage this process with the [camera](https://pub.dev/packages/camera) or [wechat_camera_picker](https://pub.dev/packages/wechat_camera_picker) package.
+### Video
+
+Video are now supported on version `3.0.0`. You can pick a video asset and select the crop area directly in the picker.
+However, as video processing is a heavy operation it is not handled by this package.
+Which means you must handle it yourself. If you want to preview the video result, you can use the `InstaAssetCropTransform` which will transform the Image or VideoPlayer to fit the selected crop area.
+
+The example app has been updated to support videos (+ camera recording) and shows [how to process the video](https://github.com/LeGoffMael/insta_assets_picker/tree/main/example/lib/post_provider.dart#L84) using [ffmpeg_kit_flutter](https://pub.dev/packages/ffmpeg_kit_flutter).
+
## ✨ Credit
This package is based on [flutter_wechat_assets_picker](https://pub.dev/packages/wechat_assets_picker) by [AlexV525](https://github.com/AlexV525) and [image_crop](https://pub.dev/packages/image_crop) by [lykhonis](https://github.com/lykhonis).
\ No newline at end of file
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
index 61b6c4d..d6afe1f 100644
--- a/example/analysis_options.yaml
+++ b/example/analysis_options.yaml
@@ -22,8 +22,8 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
- # avoid_print: false # Uncomment to disable the `avoid_print` rule
- # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+ # since it is an example app only let's keep it simple
+ depend_on_referenced_packages: ignore
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 9eed4e8..aa32b3b 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -42,9 +42,8 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.insta_assets_picker.example"
- // You can update the following values to match your application needs.
- // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
- minSdkVersion 21
+ // minSdkVersion 21
+ minSdkVersion 24 // sdk version needed for ffmpeg
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index fe7f475..f5f8f12 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,9 @@
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"/>
+
+
+
diff --git a/example/ios/Podfile b/example/ios/Podfile
index 279576f..2faca47 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '12.0'
+platform :ios, '12.1' # target required by ffmpeg
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 442a0d4..b62beef 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,6 +1,13 @@
PODS:
- camera_avfoundation (0.0.1):
- Flutter
+ - ffmpeg-kit-ios-min (6.0)
+ - ffmpeg_kit_flutter_min (6.0.3):
+ - ffmpeg_kit_flutter_min/min (= 6.0.3)
+ - Flutter
+ - ffmpeg_kit_flutter_min/min (6.0.3):
+ - ffmpeg-kit-ios-min (= 6.0)
+ - Flutter
- Flutter (1.0.0)
- insta_assets_crop (0.0.1):
- Flutter
@@ -18,6 +25,7 @@ PODS:
DEPENDENCIES:
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
+ - ffmpeg_kit_flutter_min (from `.symlinks/plugins/ffmpeg_kit_flutter_min/ios`)
- Flutter (from `Flutter`)
- insta_assets_crop (from `.symlinks/plugins/insta_assets_crop/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@@ -25,9 +33,15 @@ DEPENDENCIES:
- sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
+SPEC REPOS:
+ trunk:
+ - ffmpeg-kit-ios-min
+
EXTERNAL SOURCES:
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
+ ffmpeg_kit_flutter_min:
+ :path: ".symlinks/plugins/ffmpeg_kit_flutter_min/ios"
Flutter:
:path: Flutter
insta_assets_crop:
@@ -43,6 +57,8 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e
+ ffmpeg-kit-ios-min: 4e9a088f4ee9629435960b9d68e54848975f1931
+ ffmpeg_kit_flutter_min: 5eff47f4965bf9d1150e98961eb6129f5ae3f28c
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
insta_assets_crop: 46c0be4cbfe48cff466b6924ec93b77f656ebaa8
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
@@ -50,6 +66,6 @@ SPEC CHECKSUMS:
sensors_plus: 18a9b346c43e157da17d2c8e99def703f9efb9d8
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
-PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
+PODFILE CHECKSUM: e4638883b73f42d1731a85e8757b8cd858a2954c
COCOAPODS: 1.15.2
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 32ef87e..b41f4cc 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,21 +1,22 @@
-import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:insta_assets_picker_demo/pages/stateless_pickers.dart';
import 'package:insta_assets_picker_demo/pages/camera/camera_picker.dart';
import 'package:insta_assets_picker_demo/pages/camera/wechat_camera_picker.dart';
import 'package:insta_assets_picker_demo/pages/restorable_picker.dart';
+import 'package:insta_assets_picker_demo/post_provider.dart';
import 'package:insta_assets_picker_demo/widgets/insta_picker_interface.dart';
+import 'package:insta_assets_picker_demo/widgets/post.dart';
+import 'package:provider/provider.dart';
const kDefaultColor = Colors.deepPurple;
-late List _cameras;
-
-Future main() async {
- WidgetsFlutterBinding.ensureInitialized();
- _cameras = await availableCameras();
- runApp(const MyApp());
-}
+void main() => runApp(
+ ChangeNotifierProvider(
+ create: (context) => PostProvider(),
+ child: const MyApp(),
+ ),
+ );
class MyApp extends StatelessWidget {
const MyApp({super.key});
@@ -67,12 +68,22 @@ class PickersScreen extends StatelessWidget {
const SinglePicker(),
const MultiplePicker(),
const RestorablePicker(),
- CameraPicker(camera: _cameras.first),
+ const CameraPicker(),
const WeChatCameraPicker(),
];
return Scaffold(
- appBar: AppBar(title: const Text('Insta pickers')),
+ appBar: AppBar(
+ title: const Text('Insta pickers'),
+ actions: [
+ IconButton(
+ onPressed: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (context) => const PostsPage()),
+ ),
+ icon: Icon(Icons.feed, semanticLabel: 'Feed'),
+ )
+ ],
+ ),
body: ListView.separated(
padding: const EdgeInsets.all(16),
itemBuilder: (BuildContext context, int index) {
diff --git a/example/lib/pages/camera/camera_picker.dart b/example/lib/pages/camera/camera_picker.dart
index 09276a2..2813777 100644
--- a/example/lib/pages/camera/camera_picker.dart
+++ b/example/lib/pages/camera/camera_picker.dart
@@ -1,14 +1,15 @@
+import 'dart:io';
+import 'dart:math';
+
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:insta_assets_picker/insta_assets_picker.dart';
+import 'package:insta_assets_picker_demo/utils.dart';
import 'package:insta_assets_picker_demo/widgets/crop_result_view.dart';
import 'package:insta_assets_picker_demo/widgets/insta_picker_interface.dart';
-import 'package:path/path.dart' as path;
class CameraPicker extends StatefulWidget with InstaPickerInterface {
- const CameraPicker({super.key, required this.camera});
-
- final CameraDescription camera;
+ const CameraPicker({super.key});
@override
State createState() => _CameraPickerState();
@@ -23,19 +24,14 @@ class CameraPicker extends StatefulWidget with InstaPickerInterface {
}
class _CameraPickerState extends State {
+ late List cameras;
late CameraController _controller;
late Future _initializeControllerFuture;
@override
void initState() {
super.initState();
- _controller = CameraController(widget.camera, ResolutionPreset.max);
- _initializeControllerFuture = _controller.initialize().then((_) {
- if (!mounted) {
- return;
- }
- setState(() {});
- });
+ _initializeCamera();
}
@override
@@ -44,10 +40,28 @@ class _CameraPickerState extends State {
super.dispose();
}
+ Future _initializeCamera() async {
+ cameras = await availableCameras();
+ final int preferredIndex = cameras.indexWhere(
+ (CameraDescription e) => e.lensDirection == CameraLensDirection.back,
+ );
+
+ _controller = CameraController(
+ cameras[max(preferredIndex, 0)],
+ widget.cameraResolutionPreset,
+ );
+ _initializeControllerFuture = _controller.initialize().then((_) {
+ if (!mounted) {
+ return;
+ }
+ setState(() {});
+ });
+ }
+
/// Needs a [BuildContext] that is coming from the picker
Future _pickFromCamera(BuildContext context) async {
Feedback.forTap(context);
- final XFile? image =
+ final XFile? cameraFile =
await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (context) => CameraView(
@@ -57,20 +71,38 @@ class _CameraPickerState extends State {
),
);
- if (!context.mounted || image == null) return;
+ if (!context.mounted || cameraFile == null) return;
- final AssetEntity? entity = await PhotoManager.editor.saveImageWithPath(
- image.path,
- title: path.basename(image.path),
- );
+ AssetEntity? entity;
+ try {
+ final PermissionState ps = await PhotoManager.requestPermissionExtend();
+ if (ps == PermissionState.authorized || ps == PermissionState.limited) {
+ final File file = File(cameraFile.path);
+ final bool isVideo = isVideoFile(file);
+ final String title = getFileNameWithExtension(file);
- if (entity == null) return;
-
- if (context.mounted) {
- await InstaAssetPicker.refreshAndSelectEntity(
- context,
- entity,
- );
+ if (isVideo) {
+ entity = await PhotoManager.editor.saveVideo(file, title: title);
+ } else {
+ // NOTE: for some unknown reason, when an asset is saved
+ // from a picture took with `camera` package, it size is inversed.
+ final wrongSizeEntity = await PhotoManager.editor
+ .saveImageWithPath(file.path, title: title);
+ // TEMP FIX: Fetching it one more time seems to fix the issue
+ if (wrongSizeEntity != null) {
+ entity = await AssetEntity.fromId(wrongSizeEntity.id);
+ }
+ }
+ } else {
+ debugPrint(
+ 'Permission is not fully granted to save the captured file.');
+ }
+ } catch (e) {
+ debugPrint('Exception $e');
+ } finally {
+ if (context.mounted) {
+ await InstaAssetPicker.refreshAndSelectEntity(context, entity);
+ }
}
}
@@ -138,8 +170,8 @@ class _CameraPickerState extends State {
);
}
-/// Widget based on Flutter docs : https://docs.flutter.dev/cookbook/plugins/picture-using-camera
-class CameraView extends StatelessWidget {
+/// Widget based on camera package example : https://github.com/flutter/packages/blob/main/packages/camera/camera/example/lib/main.dart
+class CameraView extends StatefulWidget {
const CameraView({
super.key,
required this.controller,
@@ -149,35 +181,171 @@ class CameraView extends StatelessWidget {
final CameraController controller;
final Future initializeControllerFuture;
+ @override
+ State createState() => _CameraViewState();
+}
+
+class _CameraViewState extends State {
+ void onTakePictureButtonPressed() {
+ takePicture().then((XFile? file) {
+ if (mounted) {
+ if (file != null) {
+ debugPrint('Picture saved to ${file.path}');
+ }
+ Navigator.pop(context, file);
+ }
+ });
+ }
+
+ void onVideoRecordButtonPressed() {
+ startVideoRecording().then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ });
+ }
+
+ void onStopButtonPressed() {
+ stopVideoRecording().then((XFile? file) {
+ if (mounted) {
+ setState(() {});
+ if (file != null) {
+ debugPrint('Video recorded to ${file.path}');
+ }
+ Navigator.pop(context, file);
+ }
+ });
+ }
+
+ Future startVideoRecording() async {
+ // Ensure that the camera is initialized.
+ await widget.initializeControllerFuture;
+
+ if (!widget.controller.value.isInitialized) {
+ debugPrint('Error: select a camera first.');
+ return;
+ }
+
+ if (widget.controller.value.isRecordingVideo) {
+ // A recording is already started, do nothing.
+ return;
+ }
+
+ try {
+ await widget.controller.startVideoRecording();
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ return;
+ }
+ }
+
+ Future stopVideoRecording() async {
+ if (!widget.controller.value.isRecordingVideo) {
+ return null;
+ }
+
+ try {
+ return widget.controller.stopVideoRecording();
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ return null;
+ }
+ }
+
+ Future takePicture() async {
+ // Ensure that the camera is initialized.
+ await widget.initializeControllerFuture;
+
+ if (!widget.controller.value.isInitialized) {
+ debugPrint('Error: select a camera first.');
+ return null;
+ }
+
+ if (widget.controller.value.isTakingPicture) {
+ // A capture is already pending, do nothing.
+ return null;
+ }
+
+ try {
+ final XFile file = await widget.controller.takePicture();
+ return file;
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ return null;
+ }
+ }
+
+ void _showCameraException(CameraException e) {
+ final errorMsg = 'Error: ${e.code}\n${e.description}';
+ debugPrint(errorMsg);
+ }
+
+ /// Display the control bar with buttons to take pictures and record videos.
+ Widget _captureControlRowWidget() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.camera_alt),
+ onPressed: widget.controller.value.isInitialized &&
+ !widget.controller.value.isRecordingVideo
+ ? onTakePictureButtonPressed
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.videocam),
+ color: widget.controller.value.isRecordingVideo ? Colors.red : null,
+ onPressed: widget.controller.value.isInitialized &&
+ !widget.controller.value.isRecordingVideo
+ ? onVideoRecordButtonPressed
+ : onStopButtonPressed,
+ ),
+ ],
+ );
+ }
+
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(title: const Text('Take a picture')),
- body: FutureBuilder(
- future: initializeControllerFuture,
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done) {
- return CameraPreview(controller);
- } else {
- return const Center(child: CircularProgressIndicator());
- }
- },
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: () async {
- try {
- // Ensure that the camera is initialized.
- await initializeControllerFuture;
- // Attempt to take a picture and get the image's file where it was saved.
- final image = await controller.takePicture();
-
- if (!context.mounted) return;
- Navigator.pop(context, image);
- } catch (e) {
- debugPrint(e.toString());
- }
- },
- child: const Icon(Icons.camera_alt),
+ appBar: AppBar(title: const Text('Camera example')),
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: Colors.black,
+ border: Border.all(
+ color: widget.controller.value.isRecordingVideo
+ ? Colors.redAccent
+ : Colors.grey,
+ width: 3.0,
+ ),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(1.0),
+ child: FutureBuilder(
+ future: widget.initializeControllerFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ return Center(
+ child: AspectRatio(
+ aspectRatio:
+ 1 / widget.controller.value.aspectRatio,
+ child: CameraPreview(widget.controller),
+ ),
+ );
+ } else {
+ return const Center(child: CircularProgressIndicator());
+ }
+ },
+ ),
+ ),
+ ),
+ ),
+ _captureControlRowWidget(),
+ ],
+ ),
),
);
}
diff --git a/example/lib/pages/camera/wechat_camera_picker.dart b/example/lib/pages/camera/wechat_camera_picker.dart
index 7772140..b72c05a 100644
--- a/example/lib/pages/camera/wechat_camera_picker.dart
+++ b/example/lib/pages/camera/wechat_camera_picker.dart
@@ -16,12 +16,24 @@ class WeChatCameraPicker extends StatelessWidget with InstaPickerInterface {
);
/// Needs a [BuildContext] that is coming from the picker
- Future _pickFromWeChatCamera(BuildContext context) =>
- CameraPicker.pickFromCamera(
- context,
- locale: Localizations.maybeLocaleOf(context),
- pickerConfig: CameraPickerConfig(theme: Theme.of(context)),
- );
+ Future _pickFromWeChatCamera(BuildContext context) async {
+ Feedback.forTap(context);
+ final AssetEntity? entity = await CameraPicker.pickFromCamera(
+ context,
+ locale: Localizations.maybeLocaleOf(context),
+ pickerConfig: CameraPickerConfig(
+ theme: Theme.of(context),
+ resolutionPreset: cameraResolutionPreset,
+ // to allow video recording
+ enableRecording: true,
+ ),
+ );
+ if (entity == null) return;
+
+ if (context.mounted) {
+ await InstaAssetPicker.refreshAndSelectEntity(context, entity);
+ }
+ }
@override
Widget build(BuildContext context) => buildLayout(
@@ -54,19 +66,7 @@ class WeChatCameraPicker extends StatelessWidget with InstaPickerInterface {
specialItemBuilder: (context, _, __) {
// return a button that open the camera
return ElevatedButton(
- onPressed: () async {
- Feedback.forTap(context);
- final AssetEntity? entity =
- await _pickFromWeChatCamera(context);
- if (entity == null) return;
-
- if (context.mounted) {
- await InstaAssetPicker.refreshAndSelectEntity(
- context,
- entity,
- );
- }
- },
+ onPressed: () => _pickFromWeChatCamera(context),
style: ElevatedButton.styleFrom(
shape: const RoundedRectangleBorder(),
foregroundColor: Colors.white,
diff --git a/example/lib/pages/restorable_picker.dart b/example/lib/pages/restorable_picker.dart
index 6ada2a0..08649a1 100644
--- a/example/lib/pages/restorable_picker.dart
+++ b/example/lib/pages/restorable_picker.dart
@@ -19,7 +19,10 @@ class RestorablePicker extends StatefulWidget with InstaPickerInterface {
class _PickerScreenState extends State {
final _instaAssetsPicker = InstaAssetPicker();
- late final _provider = DefaultAssetPickerProvider(maxAssets: 10);
+ late final _provider = DefaultAssetPickerProvider(
+ maxAssets: 10,
+ requestType: RequestType.common,
+ );
late final ThemeData _pickerTheme = widget.getPickerTheme(context);
List selectedAssets = [];
@@ -43,7 +46,7 @@ class _PickerScreenState extends State {
),
provider: () => _provider,
onCompleted: (cropStream) {
- // example withtout StreamBuilder
+ // example without StreamBuilder
cropStream.listen((event) {
if (mounted) {
setState(() {
@@ -76,11 +79,7 @@ class _PickerScreenState extends State {
'Using this picker means that you must dispose it manually',
),
),
- CropResultView(
- selectedAssets: selectedAssets,
- croppedFiles: exportDetails?.croppedFiles ?? [],
- progress: exportDetails?.progress,
- )
+ CropResultView(result: exportDetails)
],
),
);
diff --git a/example/lib/pages/stateless_pickers.dart b/example/lib/pages/stateless_pickers.dart
index 908b37c..f1f31ef 100644
--- a/example/lib/pages/stateless_pickers.dart
+++ b/example/lib/pages/stateless_pickers.dart
@@ -8,8 +8,8 @@ class SinglePicker extends StatelessWidget with InstaPickerInterface {
PickerDescription get description => const PickerDescription(
icon: '☝️',
label: 'Single Mode Picker',
- description: 'Picker to select a single image. '
- 'Selecting a new image will replace the old one.',
+ description: 'Picker to select a single asset. '
+ 'Selecting a new asset will replace the old one.',
);
@override
@@ -29,7 +29,7 @@ class MultiplePicker extends StatelessWidget with InstaPickerInterface {
icon: '🖼️',
label: 'Multiple Mode Picker',
description:
- 'Picker for selecting multiple images (max $_kMultiplePickerMax).',
+ 'Picker for selecting multiple assets (max $_kMultiplePickerMax).',
);
@override
diff --git a/example/lib/post_provider.dart b/example/lib/post_provider.dart
new file mode 100644
index 0000000..9c316e3
--- /dev/null
+++ b/example/lib/post_provider.dart
@@ -0,0 +1,171 @@
+import 'dart:io';
+import 'package:ffmpeg_kit_flutter_min/ffmpeg_kit.dart';
+import 'package:ffmpeg_kit_flutter_min/ffmpeg_kit_config.dart';
+import 'package:ffmpeg_kit_flutter_min/return_code.dart';
+import 'package:flutter/foundation.dart';
+import 'package:insta_assets_picker/insta_assets_picker.dart';
+import 'package:insta_assets_picker_demo/utils.dart';
+import 'package:path_provider/path_provider.dart';
+
+class Post {
+ const Post({
+ required this.id,
+ required this.files,
+ required this.aspectRatio,
+ required this.createdAt,
+ });
+
+ final int id;
+ final List files;
+ final double aspectRatio;
+ final DateTime createdAt;
+}
+
+class PostProgress {
+ const PostProgress(this.postId, this.asset, this.value, [this.hasError]);
+
+ final int postId;
+ final AssetEntity asset;
+ final double value;
+ final bool? hasError;
+}
+
+class PostProvider with ChangeNotifier {
+ List _posts = [];
+ List _progress = [];
+
+ List get posts => _posts;
+ List get progress => _progress;
+
+ void add(Post post) {
+ _posts = [post, ..._posts];
+ notifyListeners();
+ // remove the progress after a delay
+ Future.delayed(Duration(seconds: 1), () => _removeProgress(post.id));
+ }
+
+ void remove(int postId) {
+ final list = [..._posts];
+ final index = list.indexWhere((p) => p.id == postId);
+ if (index == -1) return;
+ list.removeAt(index);
+ _posts = list;
+ notifyListeners();
+ }
+
+ PostProgress? _addProgress(int postId, AssetEntity asset) {
+ final list = [..._progress];
+ if (list.indexWhere((p) => p.postId == postId) != -1) return null;
+ final p = PostProgress(postId, asset, 0);
+ _progress = [p, ...list];
+ notifyListeners();
+ return p;
+ }
+
+ void _updateProgress(int postId, double value, {bool? hasError}) {
+ final list = [..._progress];
+ final index = list.indexWhere((p) => p.postId == postId);
+ if (index == -1) return;
+ list[index] = PostProgress(postId, list[index].asset, value, hasError);
+ _progress = list;
+ notifyListeners();
+ }
+
+ void _removeProgress(int postId) {
+ final list = [..._progress];
+ final index = list.indexWhere((p) => p.postId == postId);
+ if (index == -1) return;
+ list.removeAt(index);
+ _progress = list;
+ notifyListeners();
+ }
+
+ /// Crop the missing files using FFmpeg
+ Future uploadNewPost(InstaAssetsExportDetails exportDetails) async {
+ if (exportDetails.progress < 1 || exportDetails.selectedAssets.isEmpty)
+ return;
+
+ final int postId = DateTime.now().millisecondsSinceEpoch;
+ final PostProgress? progress =
+ _addProgress(postId, exportDetails.selectedAssets.first);
+
+ if (progress == null) {
+ throw 'Error: Progress already in progress';
+ }
+
+ final List files = [];
+ final double step = 1 / exportDetails.data.length;
+
+ for (int i = 0; i < exportDetails.data.length; i++) {
+ final item = exportDetails.data[i];
+
+ final double progressValue = (i + 1) * step;
+
+ if (item.croppedFile != null) {
+ files.add(item.croppedFile!);
+ _updateProgress(postId, progressValue);
+ continue;
+ }
+
+ final File? originFile = await item.selectedData.asset.originFile;
+
+ if (originFile == null) {
+ _updateProgress(postId, progressValue, hasError: true);
+ throw 'Error: File cannot be fetched';
+ }
+
+ final String extension = getFileExtension(originFile);
+ final String outputPath =
+ '${(await getTemporaryDirectory()).path}/output_${postId}_$i${extension}';
+
+ final String? ffmpegCrop = item.selectedData.ffmpegCrop;
+ final String? ffmpegScale = item.selectedData.ffmpegScale;
+ final List filters = [
+ if (ffmpegCrop != null) 'crop=${ffmpegCrop}',
+ if (ffmpegScale != null) 'scale=${ffmpegScale}'
+ ];
+
+ FFmpegKitConfig.enableStatisticsCallback((stats) {
+ final asset = exportDetails.selectedAssets[i];
+ if (asset.type != AssetType.video) return;
+ final double val = stats.getTime() / asset.duration / 1000;
+ // update progress based on ffmpeg statistics
+ _updateProgress(postId, progressValue - step + step * val.clamp(0, 1));
+ });
+ final session = await FFmpegKit.execute(
+ "-y -i \'${originFile.path}\' ${filters.isNotEmpty ? "-vf \'${filters.join(",")}\'" : ''} -c:a copy \'$outputPath\'",
+ );
+ final returnCode = await session.getReturnCode();
+
+ if (ReturnCode.isSuccess(returnCode)) {
+ // SUCCESS
+ files.add(File(outputPath));
+ _updateProgress(postId, progressValue);
+ } else if (ReturnCode.isCancel(returnCode)) {
+ // CANCEL
+ _updateProgress(postId, progressValue, hasError: true);
+ throw 'Error: FFmpeg execution got cancel.';
+ } else {
+ _updateProgress(postId, progressValue, hasError: true);
+ // ERROR
+ throw 'Error: FFmpeg failed.';
+ }
+ }
+
+ if (files.isEmpty) {
+ _updateProgress(postId, 1, hasError: true);
+ throw 'Error: result is empty';
+ }
+
+ _updateProgress(postId, 1);
+
+ add(
+ Post(
+ id: postId,
+ files: files,
+ aspectRatio: exportDetails.aspectRatio,
+ createdAt: DateTime.now(),
+ ),
+ );
+ }
+}
diff --git a/example/lib/utils.dart b/example/lib/utils.dart
new file mode 100644
index 0000000..0332fa3
--- /dev/null
+++ b/example/lib/utils.dart
@@ -0,0 +1,14 @@
+import 'dart:io';
+
+import 'package:mime/mime.dart';
+import 'package:path/path.dart' as path;
+
+/// Returns if [File] is a video based on its mime type
+bool isVideoFile(File file) =>
+ lookupMimeType(file.path)?.startsWith('video') ?? false;
+
+/// Returns [File] name with extension
+String getFileNameWithExtension(File file) => path.basename(file.path);
+
+/// Returns [File] extension in `.xxx` format
+String getFileExtension(File file) => path.extension(file.path).toLowerCase();
diff --git a/example/lib/widgets/crop_result_view.dart b/example/lib/widgets/crop_result_view.dart
index bae6537..b0de9e9 100644
--- a/example/lib/widgets/crop_result_view.dart
+++ b/example/lib/widgets/crop_result_view.dart
@@ -1,7 +1,9 @@
-import 'dart:io';
-
import 'package:flutter/material.dart';
import 'package:insta_assets_picker/insta_assets_picker.dart';
+import 'package:insta_assets_picker_demo/post_provider.dart';
+import 'package:insta_assets_picker_demo/widgets/post.dart';
+import 'package:provider/provider.dart';
+import 'package:video_player/video_player.dart';
class PickerCropResultScreen extends StatelessWidget {
const PickerCropResultScreen({super.key, required this.cropStream});
@@ -10,16 +12,14 @@ class PickerCropResultScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final height = MediaQuery.of(context).size.height - kToolbarHeight;
+ final height = MediaQuery.sizeOf(context).height - kToolbarHeight;
return Scaffold(
appBar: AppBar(title: const Text('Insta picker result')),
body: StreamBuilder(
stream: cropStream,
builder: (context, snapshot) => CropResultView(
- selectedAssets: snapshot.data?.selectedAssets ?? [],
- croppedFiles: snapshot.data?.croppedFiles ?? [],
- progress: snapshot.data?.progress,
+ result: snapshot.data,
heightFiles: height / 2,
heightAssets: height / 4,
),
@@ -31,19 +31,18 @@ class PickerCropResultScreen extends StatelessWidget {
class CropResultView extends StatelessWidget {
const CropResultView({
super.key,
- required this.selectedAssets,
- required this.croppedFiles,
- this.progress,
+ required this.result,
this.heightFiles = 300.0,
this.heightAssets = 120.0,
});
- final List selectedAssets;
- final List croppedFiles;
- final double? progress;
+ final InstaAssetsExportDetails? result;
final double heightFiles;
final double heightAssets;
+ List get data => result?.data ?? [];
+ List get selectedAssets => result?.selectedAssets ?? [];
+
Widget _buildTitle(String title, int length) {
return SizedBox(
height: 20.0,
@@ -60,10 +59,7 @@ class CropResultView extends StatelessWidget {
),
child: Text(
length.toString(),
- style: const TextStyle(
- color: Colors.white,
- height: 1.0,
- ),
+ style: const TextStyle(color: Colors.white, height: .7),
),
),
],
@@ -71,11 +67,13 @@ class CropResultView extends StatelessWidget {
);
}
- Widget _buildCroppedImagesListView(BuildContext context) {
- if (progress == null) {
+ Widget _buildCroppedAssetsListView(BuildContext context) {
+ if (result?.progress == null) {
return const SizedBox.shrink();
}
+ final double progress = result!.progress;
+
return Expanded(
child: Stack(
alignment: Alignment.center,
@@ -84,7 +82,7 @@ class CropResultView extends StatelessWidget {
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
scrollDirection: Axis.horizontal,
- itemCount: croppedFiles.length,
+ itemCount: data.length,
itemBuilder: (BuildContext _, int index) {
return Padding(
padding: const EdgeInsets.symmetric(
@@ -95,12 +93,17 @@ class CropResultView extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
),
- child: Image.file(croppedFiles[index]),
+ child: data[index]?.croppedFile != null
+ ? Image.file(data[index]!.croppedFile!)
+ : PickerResultPreview(
+ cropData: data[index]!.selectedData,
+ isAutoPlay: index == 0,
+ ),
),
);
},
),
- if (progress! < 1.0)
+ if (progress < 1.0)
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
@@ -109,7 +112,7 @@ class CropResultView extends StatelessWidget {
),
),
),
- if (progress! < 1.0)
+ if (progress < 1.0)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ClipRRect(
@@ -118,7 +121,7 @@ class CropResultView extends StatelessWidget {
height: 6,
child: LinearProgressIndicator(
value: progress,
- semanticsLabel: '${progress! * 100}%',
+ semanticsLabel: '${progress * 100}%',
),
),
),
@@ -138,7 +141,7 @@ class CropResultView extends StatelessWidget {
scrollDirection: Axis.horizontal,
itemCount: selectedAssets.length,
itemBuilder: (BuildContext _, int index) {
- final AssetEntity asset = selectedAssets.elementAt(index);
+ final AssetEntity asset = selectedAssets[index];
return Padding(
padding: const EdgeInsets.symmetric(
@@ -148,7 +151,7 @@ class CropResultView extends StatelessWidget {
// TODO : add delete action
child: RepaintBoundary(
child: ClipRRect(
- borderRadius: BorderRadius.circular(8.0),
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image(image: AssetEntityImageProvider(asset)),
),
),
@@ -166,11 +169,11 @@ class CropResultView extends StatelessWidget {
AnimatedContainer(
duration: kThemeChangeDuration,
curve: Curves.easeInOut,
- height: croppedFiles.isNotEmpty ? heightFiles : 40.0,
+ height: data.isNotEmpty ? heightFiles : 40.0,
child: Column(
children: [
- _buildTitle('Cropped Images', croppedFiles.length),
- _buildCroppedImagesListView(context),
+ _buildTitle('Crop Results', data.length),
+ _buildCroppedAssetsListView(context),
],
),
),
@@ -185,6 +188,104 @@ class CropResultView extends StatelessWidget {
],
),
),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: SizedBox(
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: result != null &&
+ result?.progress != null &&
+ result!.progress >= 1 &&
+ selectedAssets.isNotEmpty
+ ? () {
+ context.read().uploadNewPost(result!);
+ // go back to main and open post list page
+ Navigator.pushAndRemoveUntil(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => PostsPage(),
+ ),
+ (route) => route.isFirst,
+ );
+ }
+ : null,
+ icon: const Icon(Icons.cloud_upload),
+ label: const Text('Upload'),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class PickerResultPreview extends InstaAssetVideoPlayerStatefulWidget {
+ PickerResultPreview({
+ super.key,
+ required this.cropData,
+ super.isAutoPlay,
+ super.isLoop,
+ }) : super(asset: cropData.asset);
+
+ final InstaAssetsCropData cropData;
+
+ @override
+ State createState() => _PickerResultVideoPlayerState();
+}
+
+class _PickerResultVideoPlayerState extends State
+ with InstaAssetVideoPlayerMixin {
+ @override
+ Widget buildLoader() => const Center(child: CircularProgressIndicator());
+
+ @override
+ Widget buildInitializationError() =>
+ const Center(child: Text('Sorry the video could not be loaded.'));
+
+ @override
+ Widget buildVideoPlayer() {
+ return GestureDetector(
+ onTap: playButtonCallback,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ InstaAssetCropTransform(
+ asset: widget.asset,
+ cropParam: widget.cropData.cropParam,
+ child: VideoPlayer(videoController!),
+ ),
+ if (videoController != null)
+ AnimatedBuilder(
+ animation: videoController!,
+ builder: (_, __) => AnimatedOpacity(
+ opacity: isControllerPlaying ? 0 : 1,
+ duration: kThemeAnimationDuration,
+ child: CircleAvatar(
+ foregroundColor: Colors.white,
+ backgroundColor: Colors.black.withOpacity(0.7),
+ radius: 24,
+ child: const Icon(Icons.play_arrow_rounded, size: 40),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ widget.asset.type == AssetType.image
+ ? InstaAssetCropTransform(
+ asset: widget.asset,
+ cropParam: widget.cropData.cropParam,
+ child: Image(image: AssetEntityImageProvider(widget.asset)),
+ )
+ : buildDefault(),
+ const Text('⚠️ Preview ⚠️', style: TextStyle(color: Colors.redAccent)),
],
);
}
diff --git a/example/lib/widgets/insta_picker_interface.dart b/example/lib/widgets/insta_picker_interface.dart
index 8e92e82..5732371 100644
--- a/example/lib/widgets/insta_picker_interface.dart
+++ b/example/lib/widgets/insta_picker_interface.dart
@@ -1,3 +1,6 @@
+import 'dart:io';
+
+import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:insta_assets_picker/insta_assets_picker.dart';
import 'package:insta_assets_picker_demo/main.dart';
@@ -28,6 +31,11 @@ mixin InstaPickerInterface on Widget {
AppBar get _appBar => AppBar(title: Text(description.fullLabel));
+ /// NOTE: Exception on android when playing video recorded from the camera
+ /// with [ResolutionPreset.max] after FFmpeg encoding
+ ResolutionPreset get cameraResolutionPreset =>
+ Platform.isAndroid ? ResolutionPreset.high : ResolutionPreset.max;
+
Column pickerColumn({
String? text,
required VoidCallback onPressed,
@@ -83,6 +91,8 @@ mixin InstaPickerInterface on Widget {
title: description.fullLabel,
closeOnComplete: true,
pickerTheme: getPickerTheme(context),
+ // skipCropOnComplete: true, // to test ffmpeg crop image
+ // previewThumbnailSize: const ThumbnailSize(240, 240), // to improve thumbnails speed in crop view
),
maxAssets: maxAssets,
onCompleted: (Stream cropStream) {
diff --git a/example/lib/widgets/post.dart b/example/lib/widgets/post.dart
new file mode 100644
index 0000000..21738f9
--- /dev/null
+++ b/example/lib/widgets/post.dart
@@ -0,0 +1,217 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:insta_assets_picker/insta_assets_picker.dart';
+import 'package:insta_assets_picker_demo/main.dart';
+import 'package:insta_assets_picker_demo/post_provider.dart';
+import 'package:insta_assets_picker_demo/utils.dart';
+import 'package:provider/provider.dart';
+import 'package:video_player/video_player.dart';
+
+class PostsPage extends StatelessWidget {
+ const PostsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text('My Posts')),
+ body: CustomScrollView(
+ slivers: [
+ const SliverToBoxAdapter(child: PostProgressList()),
+ const PostList(),
+ ],
+ ),
+ );
+ }
+}
+
+class PostProgressList extends StatelessWidget {
+ const PostProgressList({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final List list =
+ context.select((PostProvider p) => p.progress);
+
+ return AnimatedSize(
+ duration: kThemeAnimationDuration,
+ child: list.isEmpty
+ ? const SizedBox.shrink()
+ : ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: const EdgeInsets.all(16),
+ itemBuilder: (BuildContext context, int index) {
+ final PostProgress progress = list[index];
+
+ return SizedBox(
+ height: 32,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ AspectRatio(
+ aspectRatio: 1,
+ child: ClipRRect(
+ borderRadius:
+ const BorderRadius.all(Radius.circular(4)),
+ child: Image(
+ image: AssetEntityImageProvider(progress.asset),
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: LinearProgressIndicator(
+ value: progress.value,
+ color: progress.hasError ?? false
+ ? Colors.redAccent
+ : kDefaultColor,
+ borderRadius:
+ const BorderRadius.all(Radius.circular(4)),
+ ),
+ ),
+ ),
+ progress.hasError ?? false
+ ? Icon(
+ Icons.error,
+ color: Colors.redAccent,
+ )
+ : Text(
+ '${(progress.value * 100).toInt()}%',
+ style: TextStyle(
+ color: kDefaultColor,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ separatorBuilder: (_, __) => const SizedBox(height: 4),
+ itemCount: list.length,
+ ),
+ );
+ }
+}
+
+class PostList extends StatelessWidget {
+ const PostList({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final List posts = context.select((PostProvider p) => p.posts);
+
+ if (posts.isEmpty) {
+ return SliverFillRemaining(
+ child: Center(
+ child: Text(
+ 'Empty list.\nOpen a picker to add a new post.',
+ textAlign: TextAlign.center,
+ ),
+ ),
+ );
+ }
+
+ return SliverList.separated(
+ itemCount: posts.length,
+ itemBuilder: (context, index) => PostCard(
+ key: ValueKey(posts[index].id),
+ post: posts[index],
+ ),
+ separatorBuilder: (_, __) => SizedBox(height: 32),
+ );
+ }
+}
+
+class PostCard extends StatelessWidget {
+ const PostCard({super.key, required this.post});
+
+ final Post post;
+
+ @override
+ Widget build(BuildContext context) {
+ final width = MediaQuery.sizeOf(context).width;
+
+ return SizedBox(
+ width: width,
+ height: width / post.aspectRatio,
+ child: PageView.builder(
+ itemCount: post.files.length,
+ itemBuilder: (context, index) {
+ final file = post.files[index];
+ final isVideo = isVideoFile(file);
+
+ if (isVideo) {
+ return _PostVideoPlayer(file: file);
+ }
+ return Image.file(file);
+ },
+ ),
+ );
+ }
+}
+
+class _PostVideoPlayer extends StatefulWidget {
+ const _PostVideoPlayer({required this.file});
+
+ final File file;
+
+ @override
+ State<_PostVideoPlayer> createState() => _PostVideoPlayerState();
+}
+
+/// Based on `video_player` example: https://pub.dev/packages/video_player/example
+class _PostVideoPlayerState extends State<_PostVideoPlayer> {
+ late VideoPlayerController _controller;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = VideoPlayerController.file(widget.file);
+
+ _controller.addListener(() {
+ setState(() {});
+ });
+ _controller.setLooping(true);
+ _controller.initialize().then((_) => setState(() {}));
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => Stack(
+ children: [
+ VideoPlayer(_controller),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 50),
+ reverseDuration: const Duration(milliseconds: 200),
+ child: _controller.value.isPlaying
+ ? const SizedBox.shrink()
+ : const ColoredBox(
+ color: Colors.black26,
+ child: Center(
+ child: Icon(
+ Icons.play_arrow,
+ color: Colors.white,
+ size: 40.0,
+ semanticLabel: 'Play',
+ ),
+ ),
+ ),
+ ),
+ GestureDetector(
+ onTap: () {
+ _controller.value.isPlaying
+ ? _controller.pause()
+ : _controller.play();
+ },
+ ),
+ ],
+ );
+}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index eff34e2..dc7cd04 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -137,6 +137,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
+ ffmpeg_kit_flutter_min:
+ dependency: "direct main"
+ description:
+ name: ffmpeg_kit_flutter_min
+ sha256: "123bfbc0e0b9e7cf6d32d8ba8e08b666d66af0f52c07683dd2305fbfc13f494a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.3"
+ ffmpeg_kit_flutter_platform_interface:
+ dependency: transitive
+ description:
+ name: ffmpeg_kit_flutter_platform_interface
+ sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
flutter:
dependency: "direct main"
description: flutter
@@ -217,10 +233,10 @@ packages:
dependency: transitive
description:
name: insta_assets_crop
- sha256: c6ca1786b19e0d270843346c8205b1e90418f0c5146a8a4476ba618f768b0e89
+ sha256: "1ad0627d0d79e695063be940ba9df34699ddbab2d80ccdddbf00666dda0022c5"
url: "https://pub.dev"
source: hosted
- version: "0.0.3"
+ version: "0.1.0-dev.2"
insta_assets_picker:
dependency: "direct main"
description:
@@ -308,6 +324,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
+ mime:
+ dependency: "direct main"
+ description:
+ name: mime
+ sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.5"
nested:
dependency: transitive
description:
@@ -317,7 +341,7 @@ packages:
source: hosted
version: "1.0.0"
path:
- dependency: "direct main"
+ dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index eac50a9..8166004 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -17,10 +17,13 @@ dependencies:
path: ../
# for camera examples
- path: ^1.8.3
camera: ^0.10.6
wechat_camera_picker: ^4.3.1
+ # for video examples
+ ffmpeg_kit_flutter_min: ^6.0.3
+ mime: ^1.0.5
+
dev_dependencies:
flutter_test:
sdk: flutter
diff --git a/lib/insta_assets_picker.dart b/lib/insta_assets_picker.dart
index 0bf378f..d4387c4 100644
--- a/lib/insta_assets_picker.dart
+++ b/lib/insta_assets_picker.dart
@@ -2,14 +2,11 @@ library insta_assets_picker;
export 'src/assets_picker.dart';
export 'src/insta_assets_crop_controller.dart'
- show InstaAssetsExportDetails, InstaAssetsCropData;
+ show InstaAssetsExportDetails, InstaAssetsExportData, InstaAssetsCropData;
export 'src/widget/circle_icon_button.dart';
-export 'package:wechat_assets_picker/wechat_assets_picker.dart'
- show
- AssetEntity,
- AssetEntityImageProvider,
- DefaultAssetPickerProvider,
- SpecialItemPosition,
- PhotoManager,
- PermissionState;
+export 'src/widget/crop_transform.dart';
+export 'src/widget/video_player_mixin.dart';
+
+export 'package:photo_manager/photo_manager.dart';
+export 'package:wechat_assets_picker/wechat_assets_picker.dart';
export 'package:wechat_assets_picker/src/delegates/asset_picker_text_delegate.dart';
diff --git a/lib/src/assets_picker.dart b/lib/src/assets_picker.dart
index 663c040..5728622 100644
--- a/lib/src/assets_picker.dart
+++ b/lib/src/assets_picker.dart
@@ -3,7 +3,6 @@
import 'package:flutter/material.dart';
import 'package:insta_assets_picker/insta_assets_picker.dart';
import 'package:insta_assets_picker/src/widget/insta_asset_picker_delegate.dart';
-import 'package:wechat_assets_picker/wechat_assets_picker.dart';
const _kGridCount = 4;
const _kInitializeDelayDuration = Duration(milliseconds: 250);
@@ -48,17 +47,21 @@ class InstaAssetPickerConfig {
this.themeColor,
this.textDelegate,
this.gridThumbnailSize = defaultAssetGridPreviewSize,
+ this.previewThumbnailSize,
/// [InstaAssetPickerBuilder] config
this.title,
+ this.cropDelegate = const InstaAssetCropDelegate(),
this.closeOnComplete = false,
+ this.skipCropOnComplete = false,
this.actionsBuilder,
});
/* [DefaultAssetPickerBuilderDelegate] config */
/// Specifies the number of assets in the cross axis.
+ ///
/// Defaults to [_kGridCount], like instagram.
final int gridCount;
@@ -69,6 +72,7 @@ class InstaAssetPickerConfig {
/// Set a special item in the picker with several positions.
/// Since the grid view is reversed, [SpecialItemPosition.prepend]
/// will be at the top and [SpecialItemPosition.append] at the bottom.
+ ///
/// Defaults to [SpecialItemPosition.none].
final SpecialItemPosition? specialItemPosition;
@@ -78,6 +82,7 @@ class InstaAssetPickerConfig {
/// The loader indicator to display in the picker.
final LoadingIndicatorBuilder? loadingIndicatorBuilder;
+ /// Predicate whether an asset can be selected or unselected.
final AssetSelectPredicate? selectPredicate;
/// Specifies if the limited permission overlay should be displayed.
@@ -87,22 +92,37 @@ class InstaAssetPickerConfig {
final Color? themeColor;
/// Specifies the language to apply to the picker.
+ ///
/// Default is the locale language from the context.
final AssetPickerTextDelegate? textDelegate;
/// Thumbnail size in the grid.
final ThumbnailSize gridThumbnailSize;
+ /// Preview thumbnail size in the crop viewer.
+ final ThumbnailSize? previewThumbnailSize;
+
/* [InstaAssetPickerBuilder] config */
/// Specifies the text title in the picker [AppBar].
final String? title;
+ /// Customize the display and export options of crops
+ final InstaAssetCropDelegate cropDelegate;
+
/// Specifies if the picker should be closed after assets selection confirmation.
+ ///
/// Defaults to `false`.
final bool closeOnComplete;
+ /// Specifies if the assets should be cropped when the picker is closed.
+ /// Set to `true` if you want to perform the crop yourself.
+ ///
+ /// Defaults to `false`.
+ final bool skipCropOnComplete;
+
/// The [Widget] to display on top of the assets grid view.
+ ///
/// Default is unselect all assets button.
final InstaPickerActionsBuilder? actionsBuilder;
}
@@ -146,11 +166,11 @@ class InstaAssetPicker {
/// Since the exception is thrown from the MethodChannel it cannot be caught by a try/catch
///
/// check `AssetPickerDelegate.permissionCheck()` from flutter_wechat_assets_picker package for more information.
- static Future _permissionCheck() =>
+ static Future _permissionCheck(RequestType? requestType) =>
AssetPicker.permissionCheck(
- requestOption: const PermissionRequestOption(
+ requestOption: PermissionRequestOption(
androidPermission: AndroidPermission(
- type: RequestType.image,
+ type: requestType ?? RequestType.common,
mediaLocation: false,
),
),
@@ -182,6 +202,14 @@ class InstaAssetPicker {
static ThemeData themeData(Color? themeColor, {bool light = false}) =>
AssetPicker.themeData(themeColor, light: light);
+ static void _assertRequestType(RequestType requestType) {
+ assert(
+ requestType == RequestType.image ||
+ requestType == RequestType.video ||
+ requestType == RequestType.common,
+ 'Only images and videos can be shown in the picker for now');
+ }
+
/// When using `restorableAssetsPicker` function, the picker's state is preserved even after pop
///
/// ⚠️ [InstaAssetPicker] and [provider] must be disposed manually
@@ -195,9 +223,6 @@ class InstaAssetPicker {
/// Set [onPermissionDenied] to manually handle the denied permission error.
/// The default behavior is to open a [ScaffoldMessenger].
///
- /// Crop parameters
- /// - Set [cropDelegate] to customize the display and export of crops.
- ///
/// Those arguments are used by [InstaAssetPickerBuilder]
///
/// - Set [provider] getter of type [DefaultAssetPickerProvider] to specifies picker options.
@@ -216,9 +241,6 @@ class InstaAssetPicker {
Function(BuildContext context, String delegateDescription)?
onPermissionDenied,
- /// Crop parameters
- InstaAssetCropDelegate cropDelegate = const InstaAssetCropDelegate(),
-
/// InstaAssetPickerBuilder parameters
required DefaultAssetPickerProvider Function() provider,
required Function(Stream exportDetails)
@@ -227,7 +249,7 @@ class InstaAssetPicker {
}) async {
PermissionState? ps;
try {
- ps = await _permissionCheck();
+ ps = await _permissionCheck(null);
} catch (e) {
_openErrorPermission(
context,
@@ -239,14 +261,12 @@ class InstaAssetPicker {
/// Provider must be initialized after permission check or gallery is empty (#43)
final restoredProvider = provider();
- assert(restoredProvider.requestType == RequestType.image,
- 'Only images can be shown in the picker for now');
+ _assertRequestType(restoredProvider.requestType);
builder ??= InstaAssetPickerBuilder(
initialPermission: ps,
provider: restoredProvider,
keepScrollOffset: true,
- cropDelegate: cropDelegate,
onCompleted: onCompleted,
config: pickerConfig,
locale: Localizations.maybeLocaleOf(context),
@@ -271,9 +291,6 @@ class InstaAssetPicker {
/// Set [onPermissionDenied] to manually handle the denied permission error.
/// The default behavior is to open a [ScaffoldMessenger].
///
- /// Crop options
- /// - Set [cropDelegate] to customize the display and export of crops.
- ///
/// Those arguments are used by [InstaAssetPickerBuilder]
///
/// - The [onCompleted] callback is called when the assets selection is confirmed.
@@ -306,6 +323,10 @@ class InstaAssetPicker {
///
/// - Set [initializeDelayDuration] to specifies the delay before loading the assets
/// Defaults to [_kInitializeDelayDuration].
+ ///
+ /// - Set [requestType] to specifies which type of asset to show in the picker.
+ /// Defaults is [RequestType.common]. Only [RequestType.image], [RequestType.common]
+ /// and [RequestType.common] are supported.
static Future?> pickAssets(
BuildContext context, {
Key? key,
@@ -314,9 +335,6 @@ class InstaAssetPicker {
Function(BuildContext context, String delegateDescription)?
onPermissionDenied,
- /// Crop parameters
- InstaAssetCropDelegate cropDelegate = const InstaAssetCropDelegate(),
-
/// InstaAssetPickerBuilder parameters
required Function(Stream exportDetails)
onCompleted,
@@ -332,11 +350,14 @@ class InstaAssetPicker {
bool sortPathsByModifiedDate = false,
PMFilter? filterOptions,
Duration initializeDelayDuration = _kInitializeDelayDuration,
+ RequestType requestType = RequestType.common,
}) async {
+ _assertRequestType(requestType);
+
// must be called before initializing any picker provider to avoid `PlatformException(PERMISSION_REQUESTING)` type exception
PermissionState? ps;
try {
- ps = await _permissionCheck();
+ ps = await _permissionCheck(requestType);
} catch (e) {
_openErrorPermission(
context,
@@ -351,7 +372,7 @@ class InstaAssetPicker {
maxAssets: maxAssets,
pageSize: pageSize,
pathThumbnailSize: pathThumbnailSize,
- requestType: RequestType.image,
+ requestType: requestType,
sortPathDelegate: sortPathDelegate,
sortPathsByModifiedDate: sortPathsByModifiedDate,
filterOptions: filterOptions,
@@ -362,7 +383,6 @@ class InstaAssetPicker {
initialPermission: ps,
provider: provider,
keepScrollOffset: false,
- cropDelegate: cropDelegate,
onCompleted: onCompleted,
config: pickerConfig,
locale: Localizations.maybeLocaleOf(context),
diff --git a/lib/src/insta_assets_crop_controller.dart b/lib/src/insta_assets_crop_controller.dart
index 39b5d89..5004c38 100644
--- a/lib/src/insta_assets_crop_controller.dart
+++ b/lib/src/insta_assets_crop_controller.dart
@@ -14,22 +14,37 @@ class InstaAssetsCropSingleton {
static List cropParameters = [];
}
+class InstaAssetsExportData {
+ const InstaAssetsExportData({
+ required this.croppedFile,
+ required this.selectedData,
+ });
+
+ /// The cropped file, can be null if the asset is not an image or if the
+ /// exportation was skipped ([skipCropOnComplete]=true)
+ final File? croppedFile;
+
+ /// The selected data, contains the asset and it's crop values
+ final InstaAssetsCropData selectedData;
+}
+
/// Contains all the parameters of the exportation
class InstaAssetsExportDetails {
- /// The list of the cropped files
- final List croppedFiles;
+ /// The export result, containing the selected assets, crop parameters
+ /// and possible crop file.
+ final List data;
- /// The selected thumbnails, can provided to the picker to preselect those assets
+ /// The selected thumbnails, can be provided to the picker to preselect those assets
final List selectedAssets;
- /// The selected [aspectRatio] (1 or 4/5)
+ /// The selected [aspectRatio]
final double aspectRatio;
/// The [progress] param represents progress indicator between `0.0` and `1.0`.
final double progress;
const InstaAssetsExportDetails({
- required this.croppedFiles,
+ required this.data,
required this.selectedAssets,
required this.aspectRatio,
required this.progress,
@@ -45,6 +60,27 @@ class InstaAssetsCropData {
final double scale;
final Rect? area;
+ /// Returns crop filter for ffmpeg in "out_w:out_h:x:y" format
+ String? get ffmpegCrop {
+ final area = this.area;
+ if (area == null) return null;
+
+ final w = area.width * asset.orientatedWidth;
+ final h = area.height * asset.orientatedHeight;
+ final x = area.left * asset.orientatedWidth;
+ final y = area.top * asset.orientatedHeight;
+
+ return '$w:$h:$x:$y';
+ }
+
+ /// Returns scale filter for ffmpeg in "iw*[scale]:ih*[scale]" format
+ String? get ffmpegScale {
+ final scale = cropParam?.scale;
+ if (scale == null) return null;
+
+ return 'iw*$scale:ih*$scale';
+ }
+
const InstaAssetsCropData({
required this.asset,
required this.cropParam,
@@ -73,7 +109,7 @@ class InstaAssetsCropController {
/// The index of the selected aspectRatio among the possibilities
final ValueNotifier cropRatioIndex;
- /// Whether the image in the crop view is loaded
+ /// Whether the asset in the crop view is loaded
final ValueNotifier isCropViewReady = ValueNotifier(false);
/// The asset [AssetEntity] currently displayed in the crop view
@@ -183,13 +219,14 @@ class InstaAssetsCropController {
/// Apply all the crop parameters to the list of [selectedAssets]
/// and returns the exportation as a [Stream]
Stream exportCropFiles(
- List selectedAssets,
- ) async* {
- List croppedFiles = [];
+ List selectedAssets, {
+ bool skipCrop = false,
+ }) async* {
+ final List data = [];
/// Returns the [InstaAssetsExportDetails] with given progress value [p]
InstaAssetsExportDetails makeDetail(double p) => InstaAssetsExportDetails(
- croppedFiles: croppedFiles,
+ data: data,
selectedAssets: selectedAssets,
aspectRatio: aspectRatio,
progress: p,
@@ -197,36 +234,45 @@ class InstaAssetsCropController {
// start progress
yield makeDetail(0);
- final list = cropParameters;
+ final List list = cropParameters;
final step = 1 / list.length;
- for (var i = 0; i < list.length; i++) {
- final file = await list[i].asset.originFile;
+ for (int i = 0; i < list.length; i++) {
+ final asset = list[i].asset;
- final scale = list[i].scale;
- final area = list[i].area;
+ if (skipCrop || asset.type != AssetType.image) {
+ data.add(
+ InstaAssetsExportData(croppedFile: null, selectedData: list[i]));
+ } else {
+ final file = await asset.originFile;
- if (file == null) {
- throw 'error file is null';
- }
+ final scale = list[i].scale;
+ final area = list[i].area;
- // makes the sample file to not be too small
- final sampledFile = await InstaAssetsCrop.sampleImage(
- file: file,
- preferredSize: (cropDelegate.preferredSize / scale).round(),
- );
+ if (file == null) {
+ throw 'error file is null';
+ }
- if (area == null) {
- croppedFiles.add(sampledFile);
- } else {
- // crop the file with the area selected
- final croppedFile =
- await InstaAssetsCrop.cropImage(file: sampledFile, area: area);
- // delete the not needed sample file
- sampledFile.delete();
+ // makes the sample file to not be too small
+ final sampledFile = await InstaAssetsCrop.sampleImage(
+ file: file,
+ preferredSize: (cropDelegate.preferredSize / scale).round(),
+ );
- croppedFiles.add(croppedFile);
+ if (area == null) {
+ data.add(InstaAssetsExportData(
+ croppedFile: sampledFile, selectedData: list[i]));
+ } else {
+ // crop the file with the area selected
+ final croppedFile =
+ await InstaAssetsCrop.cropImage(file: sampledFile, area: area);
+ // delete the not needed sample file
+ sampledFile.delete();
+
+ data.add(InstaAssetsExportData(
+ croppedFile: croppedFile, selectedData: list[i]));
+ }
}
// increase progress
diff --git a/lib/src/widget/crop_transform.dart b/lib/src/widget/crop_transform.dart
new file mode 100644
index 0000000..0aa72cd
--- /dev/null
+++ b/lib/src/widget/crop_transform.dart
@@ -0,0 +1,64 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:insta_assets_crop/insta_assets_crop.dart' as insta_crop_view;
+import 'package:insta_assets_picker/insta_assets_picker.dart';
+
+class InstaAssetCropTransform extends StatelessWidget {
+ const InstaAssetCropTransform({
+ super.key,
+ required this.asset,
+ required this.cropParam,
+ required this.child,
+ });
+
+ final AssetEntity asset;
+ final insta_crop_view.CropInternal? cropParam;
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ if (cropParam == null) return child;
+
+ final scale = cropParam!.scale;
+ final view = cropParam!.view;
+ final area = cropParam!.area;
+ final aspectRatio = area.size.aspectRatio;
+
+ return LayoutBuilder(builder: (_, constraints) {
+ Size size = constraints.biggest;
+ if (size.isInfinite) {
+ size = Size(
+ constraints.biggest.shortestSide,
+ constraints.biggest.shortestSide,
+ );
+ }
+
+ final ratio = max(
+ size.width / asset.orientatedSize.width,
+ size.height / asset.orientatedSize.height,
+ );
+
+ return SizedBox.fromSize(
+ size: Size(size.height * aspectRatio, size.height),
+ child: ClipRect(
+ child: FittedBox(
+ fit: BoxFit.cover,
+ child: SizedBox.fromSize(
+ size: size,
+ child: insta_crop_view.CropTransform(
+ ratio: ratio,
+ scale: scale,
+ view: view,
+ childSize: asset.orientatedSize,
+ layoutSize: size,
+ getRect: (s) => Offset.zero & s,
+ child: child,
+ ),
+ ),
+ ),
+ ),
+ );
+ });
+ }
+}
diff --git a/lib/src/widget/crop_viewer.dart b/lib/src/widget/crop_viewer.dart
index 8b31923..a9db8f7 100644
--- a/lib/src/widget/crop_viewer.dart
+++ b/lib/src/widget/crop_viewer.dart
@@ -1,11 +1,13 @@
import 'dart:math' as math;
+import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
-import 'package:insta_assets_crop/insta_assets_crop.dart';
+import 'package:insta_assets_crop/insta_assets_crop.dart' as insta_crop_view;
import 'package:insta_assets_picker/insta_assets_picker.dart';
import 'package:insta_assets_picker/src/insta_assets_crop_controller.dart';
import 'package:provider/provider.dart';
-import 'package:extended_image/extended_image.dart';
+import 'package:video_player/video_player.dart';
+import 'package:wechat_picker_library/wechat_picker_library.dart';
class CropViewer extends StatefulWidget {
const CropViewer({
@@ -17,37 +19,30 @@ class CropViewer extends StatefulWidget {
required this.height,
this.opacity = 1.0,
this.theme,
+ this.previewThumbnailSize,
});
final DefaultAssetPickerProvider provider;
-
final AssetPickerTextDelegate textDelegate;
-
final InstaAssetsCropController controller;
-
final Widget loaderWidget;
-
- final double opacity;
-
- final double height;
-
+ final double height, opacity;
final ThemeData? theme;
+ final ThumbnailSize? previewThumbnailSize;
@override
State createState() => CropViewerState();
}
class CropViewerState extends State {
- final _cropKey = GlobalKey();
+ final _cropKey = GlobalKey();
AssetEntity? _previousAsset;
- final ValueNotifier _isLoadingError = ValueNotifier(false);
@override
- void dispose() {
- // save current crop position on dispose (#25)
+ void deactivate() {
+ // save current crop position before dispose (#25)
saveCurrentCropChanges();
- _isLoadingError.dispose();
- super.dispose();
+ super.deactivate();
}
/// Save the crop parameters state in [InstaAssetsCropController]
@@ -61,118 +56,239 @@ class CropViewerState extends State {
);
}
- /// Returns the [Crop] widget
- Widget _buildCropView(AssetEntity asset, CropInternal? cropParam) => Opacity(
- opacity: widget.controller.isCropViewReady.value ? widget.opacity : 1.0,
- child: Crop(
- key: _cropKey,
- image: AssetEntityImageProvider(asset, isOriginal: true),
- placeholderWidget: ValueListenableBuilder(
- valueListenable: _isLoadingError,
- builder: (context, isLoadingError, child) => Stack(
- alignment: Alignment.center,
- children: [
- Opacity(
- opacity: widget.opacity,
- child: ExtendedImage(
- // to match crop alignment
- alignment: widget.controller.aspectRatio == 1.0
- ? Alignment.center
- : Alignment.bottomCenter,
- height: widget.height,
- width: widget.height * widget.controller.aspectRatio,
- image: AssetEntityImageProvider(asset, isOriginal: false),
- enableMemoryCache: false,
- fit: BoxFit.cover,
- ),
- ),
- // show backdrop when image is loading or if an error occured
- Positioned.fill(
- child: DecoratedBox(
- decoration: BoxDecoration(
- color: widget.theme?.cardColor.withOpacity(0.4)),
- )),
- isLoadingError
- ? Text(widget.textDelegate.loadFailed)
- : widget.loaderWidget,
- ],
- ),
- ),
- // if the image could not be loaded (i.e unsupported format like RAW)
- // unselect it and clear cache, also show the error widget
- onImageError: (exception, stackTrace) {
- widget.provider.unSelectAsset(asset);
- AssetEntityImageProvider(asset).evict();
- WidgetsBinding.instance.addPostFrameCallback((_) {
- _isLoadingError.value = true;
- widget.controller.isCropViewReady.value = true;
- });
- },
- onLoading: (isReady) => WidgetsBinding.instance.addPostFrameCallback(
- (_) => widget.controller.isCropViewReady.value = isReady),
- maximumScale: 10,
- aspectRatio: widget.controller.aspectRatio,
- disableResize: true,
- backgroundColor: widget.theme!.canvasColor,
- initialParam: cropParam,
- ),
- );
-
@override
Widget build(BuildContext context) {
+ final width = MediaQuery.sizeOf(context).width;
+
return SizedBox(
height: widget.height,
- width: MediaQuery.of(context).size.width,
+ width: width,
child: ValueListenableBuilder(
valueListenable: widget.controller.previewAsset,
builder: (_, previewAsset, __) =>
Selector>(
- selector: (_, DefaultAssetPickerProvider p) => p.selectedAssets,
- builder: (_, List selected, __) {
- _isLoadingError.value = false;
- final int effectiveIndex =
- selected.isEmpty ? 0 : selected.indexOf(selected.last);
-
- // if no asset is selected yet, returns the loader
- if (previewAsset == null && selected.isEmpty) {
- return widget.loaderWidget;
- }
-
- final asset = previewAsset ?? selected[effectiveIndex];
- final savedCropParam =
- widget.controller.get(asset)?.cropParam;
-
- // if the selected asset changed, save the previous crop parameters state
- if (asset != _previousAsset && _previousAsset != null) {
- saveCurrentCropChanges();
- }
-
- _previousAsset = asset;
-
- // don't show crop button if an asset is selected or if there is only one crop
- return selected.length > 1 ||
- widget.controller.cropDelegate.cropRatios.length <= 1
- ? _buildCropView(asset, savedCropParam)
- : ValueListenableBuilder(
- valueListenable: widget.controller.cropRatioIndex,
- builder: (context, index, child) => Stack(
- children: [
- Positioned.fill(
- child: _buildCropView(asset, savedCropParam),
- ),
- // Build crop aspect ratio button
- Positioned(
- left: 12,
- bottom: 12,
- child: _buildCropButton(),
- ),
- ],
- ),
- );
- }),
+ selector: (_, DefaultAssetPickerProvider p) => p.selectedAssets,
+ builder: (_, List selected, __) {
+ final int effectiveIndex =
+ selected.isEmpty ? 0 : selected.indexOf(selected.last);
+
+ // if no asset is selected yet, returns the loader
+ if (previewAsset == null && selected.isEmpty) {
+ return widget.loaderWidget;
+ }
+
+ final asset = previewAsset ?? selected[effectiveIndex];
+ final savedCropParam = widget.controller.get(asset)?.cropParam;
+
+ // if the selected asset changed, save the previous crop parameters state
+ if (asset != _previousAsset && _previousAsset != null) {
+ saveCurrentCropChanges();
+ }
+
+ _previousAsset = asset;
+
+ // hide crop button if an asset is selected or if there is only one crop
+ final hideCropButton = selected.length > 1 ||
+ widget.controller.cropDelegate.cropRatios.length <= 1;
+
+ return ValueListenableBuilder(
+ valueListenable: widget.controller.cropRatioIndex,
+ builder: (context, _, __) => Opacity(
+ opacity: widget.opacity,
+ child: InnerCropView(
+ cropKey: _cropKey,
+ asset: asset,
+ cropParam: savedCropParam,
+ controller: widget.controller,
+ textDelegate: widget.textDelegate,
+ theme: widget.theme,
+ height: widget.height,
+ hideCropButton: hideCropButton,
+ previewThumbnailSize: widget.previewThumbnailSize,
+ ),
+ ),
+ );
+ },
+ ),
),
);
}
+}
+
+class InnerCropView extends InstaAssetVideoPlayerStatefulWidget {
+ const InnerCropView({
+ super.key,
+ required super.asset,
+ required this.cropParam,
+ required this.controller,
+ required this.textDelegate,
+ required this.theme,
+ required this.height,
+ required this.hideCropButton,
+ required this.cropKey,
+ required this.previewThumbnailSize,
+ });
+
+ final insta_crop_view.CropInternal? cropParam;
+ final InstaAssetsCropController controller;
+ final AssetPickerTextDelegate textDelegate;
+ final ThemeData? theme;
+ final double height;
+ final bool hideCropButton;
+ final GlobalKey cropKey;
+ final ThumbnailSize? previewThumbnailSize;
+
+ @override
+ State createState() => _InnerCropViewState();
+}
+
+class _InnerCropViewState extends State
+ with InstaAssetVideoPlayerMixin {
+ final ValueNotifier _isLoadingError = ValueNotifier(false);
+
+ @override
+ void dispose() {
+ super.dispose();
+ _isLoadingError.dispose();
+ }
+
+ @override
+ void onLoading(bool isLoading) {
+ super.onLoading(isLoading);
+ WidgetsBinding.instance.addPostFrameCallback(
+ (_) => widget.controller.isCropViewReady.value = !isLoading,
+ );
+ }
+
+ @override
+ void onError(bool isError) {
+ super.onError(isError);
+ _isLoadingError.value = isError;
+ }
+
+ @override
+ Widget buildLoader() => Transform.scale(
+ scale: widget.asset.orientatedHeight / widget.height,
+ child: Image(
+ // generate video thumbnail (low quality for performances)
+ image: AssetEntityImageProvider(
+ widget.asset,
+ thumbnailSize: widget.previewThumbnailSize != null
+ ? ThumbnailSize(
+ (widget.previewThumbnailSize!.height *
+ widget.asset.orientatedSize.aspectRatio)
+ .toInt(),
+ widget.previewThumbnailSize!.height.toInt(),
+ )
+ : ThumbnailSize(
+ (widget.height * widget.asset.orientatedSize.aspectRatio)
+ .toInt(),
+ widget.height.toInt(),
+ ),
+ isOriginal: false,
+ ),
+ ),
+ );
+
+ @override
+ Widget buildInitializationError() => ScaleText(
+ widget.textDelegate.loadFailed,
+ semanticsLabel: widget.textDelegate.semanticsTextDelegate.loadFailed,
+ );
+
+ @override
+ Widget buildVideoPlayer() => VideoPlayer(videoController!);
+
+ @override
+ Widget buildDefault() {
+ if (!isLocallyAvailable && !isInitializing) {
+ initializeVideoPlayerController();
+ }
+ return AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ reverseDuration: const Duration(milliseconds: 300),
+ transitionBuilder: (Widget child, Animation animation) =>
+ FadeTransition(opacity: animation, child: child),
+ child: hasLoaded ? buildVideoPlayer() : buildLoader(),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ LocallyAvailableBuilder(
+ key: ValueKey(widget.asset.id),
+ asset: widget.asset,
+ builder: (BuildContext context, AssetEntity asset) =>
+ insta_crop_view.Crop(
+ key: widget.cropKey,
+ maximumScale: 10,
+ aspectRatio: widget.controller.aspectRatio,
+ disableResize: true,
+ backgroundColor: widget.theme!.canvasColor,
+ initialParam: widget.cropParam,
+ size: widget.asset.orientatedSize,
+ child: widget.asset.type == AssetType.image
+ ? ExtendedImage(
+ image: AssetEntityImageProvider(
+ widget.asset,
+ isOriginal: true,
+ ),
+ loadStateChanged: (ExtendedImageState state) {
+ switch (state.extendedImageLoadState) {
+ case LoadState.completed:
+ onLoading(false);
+ onError(false);
+ return state.completedWidget;
+ case LoadState.loading:
+ onLoading(true);
+ onError(false);
+ return buildLoader();
+ case LoadState.failed:
+ onLoading(false);
+ onError(true);
+ return buildLoader();
+ }
+ },
+ )
+ // build video
+ : buildDefault(),
+ ),
+ ),
+
+ ValueListenableBuilder(
+ valueListenable: _isLoadingError,
+ builder: (context, isLoadingError, __) => isLoadingError
+ ? Positioned.fill(
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: widget.theme?.cardColor.withOpacity(0.4),
+ ),
+ child: Center(child: buildInitializationError()),
+ ),
+ )
+ : const SizedBox.shrink(),
+ ),
+
+ // Build crop aspect ratio button
+ Positioned(
+ left: 12,
+ right: 12,
+ bottom: 12,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ widget.hideCropButton
+ ? const SizedBox.shrink()
+ : _buildCropButton(),
+ if (widget.asset.type == AssetType.video) _buildPlayVideoButton(),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
Widget _buildCropButton() {
return Opacity(
@@ -203,4 +319,25 @@ class CropViewerState extends State {
),
);
}
+
+ Widget _buildPlayVideoButton() {
+ if (videoController == null || !hasLoaded) return const SizedBox.shrink();
+
+ return AnimatedBuilder(
+ animation: videoController!,
+ builder: (_, __) => Opacity(
+ opacity: 0.6,
+ child: InstaPickerCircleIconButton(
+ onTap: playButtonCallback,
+ theme: widget.theme?.copyWith(
+ buttonTheme: const ButtonThemeData(padding: EdgeInsets.all(2)),
+ ),
+ size: 32,
+ icon: isControllerPlaying
+ ? const Icon(Icons.pause_rounded)
+ : const Icon(Icons.play_arrow_rounded),
+ ),
+ ),
+ );
+ }
}
diff --git a/lib/src/widget/insta_asset_picker_delegate.dart b/lib/src/widget/insta_asset_picker_delegate.dart
index 288f199..dd4718e 100644
--- a/lib/src/widget/insta_asset_picker_delegate.dart
+++ b/lib/src/widget/insta_asset_picker_delegate.dart
@@ -9,7 +9,6 @@ import 'package:insta_assets_picker/src/insta_assets_crop_controller.dart';
import 'package:insta_assets_picker/src/widget/crop_viewer.dart';
import 'package:provider/provider.dart';
-import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_picker_library/wechat_picker_library.dart';
/// The reduced height of the crop view
@@ -38,13 +37,13 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate {
required super.provider,
required this.onCompleted,
required InstaAssetPickerConfig config,
- InstaAssetCropDelegate cropDelegate = const InstaAssetCropDelegate(),
super.keepScrollOffset,
super.locale,
}) : _cropController =
- InstaAssetsCropController(keepScrollOffset, cropDelegate),
+ InstaAssetsCropController(keepScrollOffset, config.cropDelegate),
title = config.title,
closeOnComplete = config.closeOnComplete,
+ skipCropOnComplete = config.skipCropOnComplete,
actionsBuilder = config.actionsBuilder,
super(
gridCount: config.gridCount,
@@ -59,6 +58,7 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate {
themeColor: config.themeColor,
textDelegate: config.textDelegate,
gridThumbnailSize: config.gridThumbnailSize,
+ previewThumbnailSize: config.previewThumbnailSize,
shouldRevertGrid: false,
);
@@ -78,6 +78,11 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate {
/// Defaults to `false`, like instagram
final bool closeOnComplete;
+ /// Should the picker automatically crop when the selection is confirmed
+ ///
+ /// Defaults to `false`.
+ final bool skipCropOnComplete;
+
// LOCAL PARAMETERS
/// Save last position of the grid view scroll controller
@@ -112,7 +117,12 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate {
Navigator.of(context).pop(provider.selectedAssets);
}
_cropViewerKey.currentState?.saveCurrentCropChanges();
- onCompleted(_cropController.exportCropFiles(provider.selectedAssets));
+ onCompleted(
+ _cropController.exportCropFiles(
+ provider.selectedAssets,
+ skipCrop: skipCropOnComplete,
+ ),
+ );
}
/// The responsive height of the crop view
diff --git a/lib/src/widget/video_player_mixin.dart b/lib/src/widget/video_player_mixin.dart
new file mode 100644
index 0000000..898e5ab
--- /dev/null
+++ b/lib/src/widget/video_player_mixin.dart
@@ -0,0 +1,163 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:insta_assets_picker/insta_assets_picker.dart';
+import 'package:video_player/video_player.dart';
+import 'package:wechat_picker_library/wechat_picker_library.dart';
+
+abstract class InstaAssetVideoPlayerStatefulWidget extends StatefulWidget {
+ const InstaAssetVideoPlayerStatefulWidget({
+ super.key,
+ required this.asset,
+ this.isLoop = false,
+ this.isAutoPlay = false,
+ });
+
+ final AssetEntity asset;
+
+ final bool isLoop;
+ final bool isAutoPlay;
+}
+
+/// Based on _VideoPageBuilderState from wechat_assets_picker: https://github.com/fluttercandies/flutter_wechat_assets_picker/blob/main/lib/src/widget/builder/video_page_builder.dart
+mixin InstaAssetVideoPlayerMixin
+ on State {
+ /// Controller for the video player.
+ VideoPlayerController? videoController;
+
+ /// Whether the controller has initialized.
+ bool hasLoaded = false;
+
+ /// Whether there's any error when initialize the video controller.
+ bool hasErrorWhenInitializing = false;
+
+ /// Whether the controller is playing.
+ bool get isControllerPlaying => videoController?.value.isPlaying ?? false;
+
+ bool isInitializing = false;
+ bool isLocallyAvailable = false;
+
+ @override
+ void didUpdateWidget(T oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.asset != oldWidget.asset) {
+ videoController
+ ?..pause()
+ ..dispose();
+ videoController = null;
+ hasLoaded = false;
+ isInitializing = false;
+ isLocallyAvailable = false;
+ onError(false);
+ onLoading(false);
+ }
+ }
+
+ @override
+ void dispose() {
+ videoController
+ ?..pause()
+ ..dispose();
+ super.dispose();
+ }
+
+ Future initializeVideoPlayerController() async {
+ isInitializing = true;
+ isLocallyAvailable = true;
+ onLoading(true);
+ final String? url = await widget.asset.getMediaUrl();
+ if (url == null) {
+ onError(true);
+ if (mounted) {
+ setState(() {});
+ }
+ onLoading(false);
+ return;
+ }
+ final Uri uri = Uri.parse(url);
+ if (Platform.isAndroid) {
+ videoController = VideoPlayerController.contentUri(uri);
+ } else {
+ videoController = VideoPlayerController.networkUrl(uri);
+ }
+
+ try {
+ await videoController?.initialize();
+ hasLoaded = true;
+ videoController?.setLooping(widget.isLoop);
+ if (widget.isAutoPlay) {
+ videoController?.play();
+ }
+ } catch (e, s) {
+ FlutterError.presentError(
+ FlutterErrorDetails(
+ exception: e,
+ stack: s,
+ library: 'insta_assets_picker',
+ silent: true,
+ ),
+ );
+ onError(true);
+ } finally {
+ if (mounted) {
+ setState(() {});
+ }
+ }
+ onLoading(false);
+ }
+
+ /// Callback for the play button.
+ ///
+ /// Normally it only switches play state for the player. If the video reaches the end,
+ /// then click the button will make the video replay.
+ Future playButtonCallback() async {
+ if (videoController == null) return;
+ if (isControllerPlaying) {
+ videoController?.pause();
+ return;
+ }
+ if (videoController?.value.duration == videoController?.value.position) {
+ videoController
+ ?..seekTo(Duration.zero)
+ ..play();
+ return;
+ }
+ videoController?.play();
+ }
+
+ void onLoading(bool isLoading) {}
+ void onError(bool isError) {
+ hasErrorWhenInitializing = isError;
+ }
+
+ Widget buildLoader();
+ Widget buildInitializationError();
+ Widget buildVideoPlayer();
+
+ Widget buildDefault() => buildVideoPlayerBuilder(
+ builder: (BuildContext context, AssetEntity asset) {
+ if (hasErrorWhenInitializing) {
+ return buildInitializationError();
+ }
+ if (!isLocallyAvailable && !isInitializing) {
+ initializeVideoPlayerController();
+ }
+ if (!hasLoaded) {
+ return buildLoader();
+ }
+ return buildVideoPlayer();
+ },
+ );
+
+ Widget buildVideoPlayerBuilder({
+ required Widget Function(BuildContext, AssetEntity) builder,
+ }) =>
+ LocallyAvailableBuilder(
+ key: ValueKey(widget.asset.id),
+ asset: widget.asset,
+ builder: builder,
+ );
+
+ @override
+ Widget build(BuildContext context) => buildDefault();
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index d029b44..fd96245 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,12 @@
name: insta_assets_picker
-description: An image picker similar with Instagram, supports multi picking, crop and aspect ratio.
+description: An image (and videos) picker similar with Instagram, supports multi picking, crop and aspect ratio.
version: 2.3.1
repository: https://github.com/LeGoffMael/insta_assets_picker
topics:
- picker
- crop
- image
+ - video
- instagram
environment:
@@ -16,12 +17,17 @@ dependencies:
flutter:
sdk: flutter
- insta_assets_crop: ^0.0.3 # custom package from image_crop
+ insta_assets_crop: ^0.1.0-dev.2 # custom package from image_crop
fraction: ^5.0.2 # to show crop ratio in crop view
+
wechat_assets_picker: ^9.1.0
+
+ # match with wechat_assets_picker package
+ photo_manager: ^3.0.0
wechat_picker_library: ^1.0.0
- provider: ^6.0.5 # match with wechat_assets_picker package
- extended_image: ^8.2.0 # match with wechat_assets_picker package
+ extended_image: ^8.2.0
+ provider: ^6.0.5
+ video_player: ^2.7.0
dev_dependencies:
flutter_test: