diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 27d3e42..b89059b 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -10,21 +10,28 @@ android {
namespace = "com.org.yumi"
compileSdk = 36
ndkVersion = "28.2.13676358"
-
- signingConfigs {
- create("release") {
- storeFile = file("./yumi.jks")
- storePassword = "2025abc"
- keyAlias = "yumi"
- keyPassword = "2025abc"
- }
-
- getByName("debug") {
- storeFile = file("./yumi_debug.jks")
- storePassword = "2025abc"
- keyAlias = "yumi"
- keyPassword = "2025abc"
- }
+
+ val releaseKeystoreFile = file("./yumi.jks")
+ val debugKeystoreFile = file("./yumi_debug.jks")
+
+ signingConfigs {
+ create("release") {
+ if (releaseKeystoreFile.exists()) {
+ storeFile = releaseKeystoreFile
+ storePassword = "2025abc"
+ keyAlias = "yumi"
+ keyPassword = "2025abc"
+ }
+ }
+
+ getByName("debug") {
+ if (debugKeystoreFile.exists()) {
+ storeFile = debugKeystoreFile
+ storePassword = "2025abc"
+ keyAlias = "yumi"
+ keyPassword = "2025abc"
+ }
+ }
}
compileOptions {
@@ -60,22 +67,26 @@ android {
}
buildTypes {
- getByName("release") {
- isMinifyEnabled = false
- isShrinkResources = false
- isZipAlignEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- signingConfig = signingConfigs["release"]
- }
- getByName("debug") {
- isMinifyEnabled = false
- signingConfig = signingConfigs["release"]
- }
- }
-}
+ getByName("release") {
+ isMinifyEnabled = false
+ isShrinkResources = false
+ isZipAlignEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ signingConfig = if (releaseKeystoreFile.exists()) {
+ signingConfigs["release"]
+ } else {
+ signingConfigs["debug"]
+ }
+ }
+ getByName("debug") {
+ isMinifyEnabled = false
+ signingConfig = signingConfigs["debug"]
+ }
+ }
+}
dependencies {
implementation("androidx.multidex:multidex:2.0.1")
// implementation(platform("com.google.firebase:firebase-bom:34.0.0"))
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 09a35e3..1a11105 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -9,26 +9,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -118,17 +104,11 @@
android:name="com.google.android.gms.auth.api.signin.GoogleSignInActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
-
-
-
-
-
+
+
+
+
+
+
+
+ ....
+
+```
+
+## Usage
+
+### Required parameters
+
+* **sourcePath**: the absolute path of an image file.
+
+### Optional parameters
+
+* **maxWidth**: maximum cropped image width. Note: this field is ignored on Web.
+
+* **maxHeight**: maximum cropped image height. Note: this field is ignored on Web.
+
+* **aspectRatio**: controls the aspect ratio of crop bounds. If this values is set, the cropper is locked and user can't change the aspect ratio of crop bounds. Note: this field is ignored on Web.
+
+* **aspectRatioPresets**: controls the list of aspect ratios in the crop menu view. In Android, you can set the initialized aspect ratio when starting the cropper by setting the value of `AndroidUiSettings.initAspectRatio`. Note: this field is ignored on Web.
+
+* **cropStyle**: controls the style of crop bounds, it can be rectangle or circle style (default is `CropStyle.rectangle`). Note: this field can be overrided by `WebUiSettings.viewPort.type` on Web
+
+* **compressFormat**: the format of result image, png or jpg (default is ImageCompressFormat.jpg).
+
+* **compressQuality**: number between 0 and 100 to control the quality of image compression.
+
+* **uiSettings**: controls UI customization on specific platform (android, ios, web,...)
+
+ * Android: see [Android customization](#android-1).
+
+ * iOS: see [iOS customization](#ios-1).
+
+ * Web: see [Web customization](#web-1).
+
+### Note
+
+* The result file is saved in `NSTemporaryDirectory` on iOS and application Cache directory on Android, so it can be lost later, you are responsible for storing it somewhere permanent (if needed).
+
+* The implementation on Web is much different compared to the implementation on mobile app. It causes some configuration fields not working (`maxWidth`, `maxHeight`, `aspectRatio`, `aspectRatioPresets`) on Web.
+
+* `WebUiSettings` is required for Web.
+
+## Customization
+
+### Android
+
+
+Click to view detail
+
+
+**Image Cropper** provides a helper class called `AndroidUiSettings` that wraps all properties can be used to customize UI in **uCrop** library.
+
+| Property | Description | Type |
+|-----------------------------|-------------------------------------------------------------------------------------------------------------|-----------------------|
+| `toolbarTitle` | desired text for Toolbar title | String |
+| `toolbarColor` | desired color of the Toolbar | Color |
+| `statusBarColor` | desired color of status | Color |
+| `toolbarWidgetColor` | desired color of Toolbar text and buttons (default is darker orange) | Color |
+| `backgroundColor` | desired background color that should be applied to the root view | Color |
+| `activeControlsWidgetColor` | desired resolved color of the active and selected widget and progress wheel middle line (default is white) | Color |
+| `dimmedLayerColor` | desired color of dimmed area around the crop bounds | Color |
+| `cropFrameColor` | desired color of crop frame | Color |
+| `cropGridColor` | desired color of crop grid/guidelines | Color |
+| `cropFrameStrokeWidth` | desired width of crop frame line in pixels | int |
+| `cropGridRowCount` | crop grid rows count | int |
+| `cropGridColumnCount` | crop grid columns count | int |
+| `cropGridStrokeWidth` | desired width of crop grid lines in pixels | int |
+| `showCropGrid` | set to true if you want to see a crop grid/guidelines on top of an image | bool |
+| `lockAspectRatio` | set to true if you want to lock the aspect ratio of crop bounds with a fixed value (locked by default) | bool |
+| `hideBottomControls` | set to true to hide the bottom controls (shown by default) | bool |
+| `initAspectRatio` | desired aspect ratio is applied (from the list of given aspect ratio presets) when starting the cropper | CropAspectRatioPreset |
+
+
+
+### iOS
+
+
+Click to view detail
+
+
+**Image Cropper** provides a helper class called `IOUiSettings` that wraps all properties can be used to customize UI in **TOCropViewController** library.
+
+| Property | Description | Type |
+|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|
+| `minimumAspectRatio` | The minimum croping aspect ratio. If set, user is prevented from setting cropping rectangle to lower aspect ratio than defined by the parameter | double |
+| `rectX` | The initial rect of cropping: x. | double |
+| `rectY` | The initial rect of cropping: y. | double |
+| `rectWidth` | The initial rect of cropping: width. | double |
+| `rectHeight` | The initial rect of cropping: height. | double |
+| `showActivitySheetOnDone` | If true, when the user hits 'Done', a `UIActivityController` will appear before the view controller ends | bool |
+| `showCancelConfirmationDialog` | Shows a confirmation dialog when the user hits 'Cancel' and there are pending changes (default is false) | bool |
+| `rotateClockwiseButtonHidden` | When disabled, an additional rotation button that rotates the canvas in 90-degree segments in a clockwise direction is shown in the toolbar (default is false) | bool |
+| `hidesNavigationBar` | If this controller is embedded in `UINavigationController` its navigation bar is hidden by default. Set this property to false to show the navigation bar. This must be set before this controller is presented | bool |
+| `rotateButtonsHidden` | When enabled, hides the rotation button, as well as the alternative rotation button visible when `showClockwiseRotationButton` is set to YES (default is false) | bool |
+| `resetButtonHidden` | When enabled, hides the 'Reset' button on the toolbar (default is false) | bool |
+| `aspectRatioPickerButtonHidden` | When enabled, hides the 'Aspect Ratio Picker' button on the toolbar (default is false) | bool |
+| `resetAspectRatioEnabled` | If true, tapping the reset button will also reset the aspect ratio back to the image default ratio. Otherwise, the reset will just zoom out to the current aspect ratio. If this is set to false, and `aspectRatioLockEnabled` is set to true, then the aspect ratio button will automatically be hidden from the toolbar (default is true) | bool |
+| `aspectRatioLockDimensionSwapEnabled` | If true, a custom aspect ratio is set, and the `aspectRatioLockEnabled` is set to true, the crop box will swap it's dimensions depending on portrait or landscape sized images. This value also controls whether the dimensions can swap when the image is rotated (default is false) | bool |
+| `aspectRatioLockEnabled` | If true, while it can still be resized, the crop box will be locked to its current aspect ratio. If this is set to true, and `resetAspectRatioEnabled` is set to false, then the aspect ratio button will automatically be hidden from the toolbar (default is false) | bool |
+| `title` | Title text that appears at the top of the view controller. | String |
+| `doneButtonTitle` | Title for the 'Done' button. Setting this will override the Default which is a localized string for "Done" | String |
+| `cancelButtonTitle` | Title for the 'Cancel' button. Setting this will override the Default which is a localized string for "Cancel" | String |
+
+
+
+### Web
+
+
+Click to view detail
+
+
+**Image Cropper** provides a helper class called `WebUiSettings` that wraps all properties can be used to customize UI in **croppie** library.
+
+| Property | Description | Type |
+|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------|
+| `boundary` | The outer container of the cropper. Default = { width: 500, height: 500 } | Boundary |
+| `viewPort` | The inner container of the coppie. The visible part of the image. Valid type values:'square' 'circle'. Default = { width: 400, height: 400, type: 'square' } | ViewPort |
+| `customClass` | A class of your choosing to add to the container to add custom styles to your croppie. Default = '' | String |
+| `enableExif` | Enable exif orientation reading. Tells Croppie to read exif orientation from the image data and orient the image correctly before rendering to the page. Requires exif.js (packages/croppie_dart/lib/src/exif.js) | bool |
+| `enableOrientation` | Enable or disable support for specifying a custom orientation when binding images. Default = true | bool |
+| `enableZoom` | Enable zooming functionality. If set to false - scrolling and pinching would not zoom. Default = false | bool |
+| `enableResize` | Enable or disable support for resizing the viewport area. Default = false | bool |
+| `mouseWheelZoom` | Enable or disable the ability to use the mouse wheel to zoom in and out on a croppie instance. Default = true | bool |
+| `showZoomer` | Hide or show the zoom slider. Default = true | bool |
+| `presentStyle` | Presentation style of cropper, either a dialog or a page (route). Default = dialog | CropperPresentStyle |
+| `context` | Current BuildContext. The context is required to show cropper dialog or route | BuildContext |
+| `customDialogBuilder` | Builder to customize the cropper Dialog | CropperDialogBuilder |
+| `customRouteBuilder` | Builder to customize the cropper PageRoute | CropperRouteBuilder |
+
+#### Note:
+
+If using `CropperDialogBuilder` and `CropperRouteBuilder` to customize cropper dialog and route, the customization codes need to call `crop()` function to trigger crop feature and then returning the cropped result data to the plugin by using `Navigator.of(context).pop(result)`.
+
+````dart
+
+ WebUiSettings(
+ ...
+ customDialogBuilder: (cropper, crop, rotate) {
+ return Dialog(
+ child: Builder(
+ builder: (context) {
+ return Column(
+ children: [
+ ...
+ cropper,
+ ...
+ TextButton(
+ onPressed: () async {
+ /// it is important to call crop() function and return
+ /// result data to plugin, for example:
+ final result = await crop();
+ Navigator.of(context).pop(result);
+ },
+ child: Text('Crop'),
+ )
+ ]
+ );
+ },
+ ),
+ );
+ },
+ ...
+ )
+
+````
+
+
+
+## Example
+
+````dart
+
+import 'package:image_cropper/image_cropper.dart';
+
+CroppedFile croppedFile = await ImageCropper().cropImage(
+ sourcePath: imageFile.path,
+ aspectRatioPresets: [
+ CropAspectRatioPreset.square,
+ CropAspectRatioPreset.ratio3x2,
+ CropAspectRatioPreset.original,
+ CropAspectRatioPreset.ratio4x3,
+ CropAspectRatioPreset.ratio16x9
+ ],
+ uiSettings: [
+ AndroidUiSettings(
+ toolbarTitle: 'Cropper',
+ toolbarColor: Colors.deepOrange,
+ toolbarWidgetColor: Colors.white,
+ initAspectRatio: CropAspectRatioPreset.original,
+ lockAspectRatio: false),
+ IOSUiSettings(
+ title: 'Cropper',
+ ),
+ WebUiSettings(
+ context: context,
+ ),
+ ],
+ );
+
+````
+
+## Credits
+
+- Android: [uCrop](https://github.com/Yalantis/uCrop) created by [Yalantis](https://github.com/Yalantis)
+- iOS: [TOCropViewController](https://github.com/TimOliver/TOCropViewController) created by [Tim Oliver](https://twitter.com/TimOliverAU)
+- Web: [croppie](https://github.com/Foliotek/Croppie) created by [Foliotek](https://github.com/Foliotek) and [croppie-dart](https://gitlab.com/michel.werren/croppie-dart) created by [Michel Werren](https://gitlab.com/michel.werren)
diff --git a/local_packages/image_cropper-5.0.1-patched/android/build.gradle b/local_packages/image_cropper-5.0.1-patched/android/build.gradle
new file mode 100644
index 0000000..9a437c3
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/build.gradle
@@ -0,0 +1,43 @@
+group 'vn.hunghd.flutter.plugins.imagecropper'
+version '1.0-SNAPSHOT'
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://www.jitpack.io" }
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.6.4'
+ }
+}
+
+rootProject.allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://www.jitpack.io" }
+ }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+ // Conditional for compatibility with AGP <4.2.
+ if (project.android.hasProperty("namespace")) {
+ namespace 'vn.hunghd.flutter.plugins.imagecropper'
+ }
+ compileSdkVersion 31
+
+ defaultConfig {
+ minSdkVersion 16
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+ dependencies {
+ implementation 'com.github.yalantis:ucrop:2.2.8'
+ }
+}
diff --git a/local_packages/image_cropper-5.0.1-patched/android/gradle.properties b/local_packages/image_cropper-5.0.1-patched/android/gradle.properties
new file mode 100644
index 0000000..53ae0ae
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/gradle.properties
@@ -0,0 +1,3 @@
+android.enableJetifier=true
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx1536M
diff --git a/local_packages/image_cropper-5.0.1-patched/android/gradle/wrapper/gradle-wrapper.jar b/local_packages/image_cropper-5.0.1-patched/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/local_packages/image_cropper-5.0.1-patched/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/local_packages/image_cropper-5.0.1-patched/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/image_cropper-5.0.1-patched/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e42f9a2
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Mar 29 16:49:03 EET 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/local_packages/image_cropper-5.0.1-patched/android/gradlew b/local_packages/image_cropper-5.0.1-patched/android/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/local_packages/image_cropper-5.0.1-patched/android/settings.gradle b/local_packages/image_cropper-5.0.1-patched/android/settings.gradle
new file mode 100644
index 0000000..8228025
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'image_cropper'
diff --git a/local_packages/image_cropper-5.0.1-patched/android/src/main/AndroidManifest.xml b/local_packages/image_cropper-5.0.1-patched/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f6e0a3f
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/FileUtils.java b/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/FileUtils.java
new file mode 100644
index 0000000..641db14
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/FileUtils.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2007-2008 OpenIntents.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This file was modified by the Flutter authors from the following original file:
+ * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
+ */
+
+package vn.hunghd.flutter.plugins.imagecropper;
+
+import android.annotation.SuppressLint;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class FileUtils {
+
+ String getPathFromUri(final Context context, final Uri uri) {
+ String path = getPathFromLocalUri(context, uri);
+ if (path == null) {
+ path = getPathFromRemoteUri(context, uri);
+ }
+ return path;
+ }
+
+ @SuppressLint("NewApi")
+ private String getPathFromLocalUri(final Context context, final Uri uri) {
+ final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+
+ if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
+ } else if (isDownloadsDocument(uri)) {
+ final String id = DocumentsContract.getDocumentId(uri);
+
+ if (!TextUtils.isEmpty(id)) {
+ try {
+ final Uri contentUri =
+ ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+ return getDataColumn(context, contentUri, null, null);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ } else if (isMediaDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[] {split[1]};
+
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ } else if ("content".equalsIgnoreCase(uri.getScheme())) {
+
+ // Return the remote address
+ if (isGooglePhotosUri(uri)) {
+ return uri.getLastPathSegment();
+ }
+
+ return getDataColumn(context, uri, null, null);
+ } else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ private static String getDataColumn(
+ Context context, Uri uri, String selection, String[] selectionArgs) {
+ Cursor cursor = null;
+
+ final String column = "_data";
+ final String[] projection = {column};
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int column_index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(column_index);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return null;
+ }
+
+ private static String getPathFromRemoteUri(final Context context, final Uri uri) {
+ // The code below is why Java now has try-with-resources and the Files utility.
+ File file = null;
+ InputStream inputStream = null;
+ OutputStream outputStream = null;
+ boolean success = false;
+ try {
+ inputStream = context.getContentResolver().openInputStream(uri);
+ file = File.createTempFile("image_picker", "jpg", context.getCacheDir());
+ outputStream = new FileOutputStream(file);
+ if (inputStream != null) {
+ copy(inputStream, outputStream);
+ success = true;
+ }
+ } catch (IOException ignored) {
+ } finally {
+ try {
+ if (inputStream != null) inputStream.close();
+ } catch (IOException ignored) {
+ }
+ try {
+ if (outputStream != null) outputStream.close();
+ } catch (IOException ignored) {
+ // If closing the output stream fails, we cannot be sure that the
+ // target file was written in full. Flushing the stream merely moves
+ // the bytes into the OS, not necessarily to the file.
+ success = false;
+ }
+ }
+ return success ? file.getPath() : null;
+ }
+
+ private static void copy(InputStream in, OutputStream out) throws IOException {
+ final byte[] buffer = new byte[4 * 1024];
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) != -1) {
+ out.write(buffer, 0, bytesRead);
+ }
+ out.flush();
+ }
+
+ private static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
+
+ private static boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
+
+ private static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+
+ private static boolean isGooglePhotosUri(Uri uri) {
+ return "com.google.android.apps.photos.content".equals(uri.getAuthority());
+ }
+}
diff --git a/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/ImageCropperDelegate.java b/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/ImageCropperDelegate.java
new file mode 100644
index 0000000..0377bbd
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/ImageCropperDelegate.java
@@ -0,0 +1,266 @@
+package vn.hunghd.flutter.plugins.imagecropper;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+
+import com.yalantis.ucrop.UCrop;
+import com.yalantis.ucrop.model.AspectRatio;
+import com.yalantis.ucrop.view.CropImageView;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Date;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+
+import static android.app.Activity.RESULT_OK;
+
+public class ImageCropperDelegate implements PluginRegistry.ActivityResultListener {
+ static final String FILENAME_CACHE_KEY = "imagecropper.FILENAME_CACHE_KEY";
+
+ private final Activity activity;
+ private final SharedPreferences preferences;
+ private MethodChannel.Result pendingResult;
+ private FileUtils fileUtils;
+
+ public ImageCropperDelegate(Activity activity) {
+ this.activity = activity;
+ preferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
+ fileUtils = new FileUtils();
+ }
+
+ public void startCrop(MethodCall call, MethodChannel.Result result) {
+ String sourcePath = call.argument("source_path");
+ Integer maxWidth = call.argument("max_width");
+ Integer maxHeight = call.argument("max_height");
+ Double ratioX = call.argument("ratio_x");
+ Double ratioY = call.argument("ratio_y");
+ String cropStyle = call.argument("crop_style");
+ String compressFormat = call.argument("compress_format");
+ Integer compressQuality = call.argument("compress_quality");
+ ArrayList aspectRatioPresets = call.argument("aspect_ratio_presets");
+ String initAspectRatio = call.argument("android.init_aspect_ratio");
+
+ pendingResult = result;
+
+ File outputDir = activity.getCacheDir();
+ File outputFile;
+ if("png".equals(compressFormat)){
+ outputFile = new File(outputDir, "image_cropper_" + (new Date()).getTime() + ".png");
+ } else {
+ outputFile = new File(outputDir, "image_cropper_" + (new Date()).getTime() + ".jpg");
+ }
+ Uri sourceUri = Uri.fromFile(new File(sourcePath));
+ Uri destinationUri = Uri.fromFile(outputFile);
+
+ UCrop.Options options = new UCrop.Options();
+ // uCrop.withMaxResultSize(1000, 1000);
+ options.setCompressionFormat("png".equals(compressFormat) ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG);
+ options.setCompressionQuality(compressQuality != null ? compressQuality : 90);
+ options.setMaxBitmapSize(10000);
+
+ // UI customization settings
+ if ("circle".equals(cropStyle)) {
+ options.setCircleDimmedLayer(true);
+ }
+ setupUiCustomizedOptions(options, call);
+
+ if (aspectRatioPresets != null) {
+ ArrayList aspectRatioList = new ArrayList<>();
+ int defaultIndex = 0;
+ for (int i = 0; i < aspectRatioPresets.size(); i++) {
+ String preset = aspectRatioPresets.get(i);
+ if (preset != null) {
+ AspectRatio aspectRatio = parseAspectRatioName(preset);
+ aspectRatioList.add(aspectRatio);
+ if (preset.equals(initAspectRatio)) {
+ defaultIndex = i;
+ }
+ }
+ }
+ options.setAspectRatioOptions(defaultIndex, aspectRatioList.toArray(new AspectRatio[]{}));
+ }
+
+ UCrop cropper = UCrop.of(sourceUri, destinationUri).withOptions(options);
+ if (maxWidth != null && maxHeight != null) {
+ cropper.withMaxResultSize(maxWidth, maxHeight);
+ }
+ if (ratioX != null && ratioY != null) {
+ cropper.withAspectRatio(ratioX.floatValue(), ratioY.floatValue());
+ }
+
+ activity.startActivityForResult(cropper.getIntent(activity), UCrop.REQUEST_CROP);
+ }
+
+ public void recoverImage(MethodCall call, MethodChannel.Result result) {
+ result.success(getAndClearCachedImage());
+ }
+
+ private void cacheImage(String filePath) {
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(FILENAME_CACHE_KEY, filePath);
+ editor.apply();
+ }
+
+ private String getAndClearCachedImage() {
+ if (preferences.contains(FILENAME_CACHE_KEY)) {
+ String result = preferences.getString(FILENAME_CACHE_KEY, "");
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.remove(FILENAME_CACHE_KEY);
+ editor.apply();
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == UCrop.REQUEST_CROP) {
+ if (resultCode == RESULT_OK) {
+ final Uri resultUri = UCrop.getOutput(data);
+ final String imagePath = fileUtils.getPathFromUri(activity, resultUri);
+ cacheImage(imagePath);
+ finishWithSuccess(imagePath);
+ return true;
+ } else if (resultCode == UCrop.RESULT_ERROR) {
+ final Throwable cropError = UCrop.getError(data);
+ finishWithError("crop_error", cropError.getLocalizedMessage(), cropError);
+ return true;
+ } else if (pendingResult != null) {
+ pendingResult.success(null);
+ clearMethodCallAndResult();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void finishWithSuccess(String imagePath) {
+ if (pendingResult != null) {
+ pendingResult.success(imagePath);
+ clearMethodCallAndResult();
+ }
+ }
+
+ private void finishWithError(String errorCode, String errorMessage, Throwable throwable) {
+ if (pendingResult != null) {
+ pendingResult.error(errorCode, errorMessage, throwable);
+ clearMethodCallAndResult();
+ }
+ }
+
+ private UCrop.Options setupUiCustomizedOptions(UCrop.Options options, MethodCall call) {
+ String title = call.argument("android.toolbar_title");
+ Integer toolbarColor = call.argument("android.toolbar_color");
+ Integer statusBarColor = call.argument("android.statusbar_color");
+ Integer toolbarWidgetColor = call.argument("android.toolbar_widget_color");
+ Integer backgroundColor = call.argument("android.background_color");
+ Integer activeControlsWidgetColor = call.argument("android.active_controls_widget_color");
+ Integer dimmedLayerColor = call.argument("android.dimmed_layer_color");
+ Integer cropFrameColor = call.argument("android.crop_frame_color");
+ Integer cropGridColor = call.argument("android.crop_grid_color");
+ Integer cropFrameStrokeWidth = call.argument("android.crop_frame_stroke_width");
+ Integer cropGridRowCount = call.argument("android.crop_grid_row_count");
+ Integer cropGridColumnCount = call.argument("android.crop_grid_column_count");
+ Integer cropGridStrokeWidth = call.argument("android.crop_grid_stroke_width");
+ Boolean showCropGrid = call.argument("android.show_crop_grid");
+ Boolean lockAspectRatio = call.argument("android.lock_aspect_ratio");
+ Boolean hideBottomControls = call.argument("android.hide_bottom_controls");
+
+ if (title != null) {
+ options.setToolbarTitle(title);
+ }
+ if (toolbarColor != null) {
+ options.setToolbarColor(toolbarColor);
+ }
+ if (statusBarColor != null) {
+ options.setStatusBarColor(statusBarColor);
+ } else if (toolbarColor != null) {
+ options.setStatusBarColor(darkenColor(toolbarColor));
+ }
+ if (toolbarWidgetColor != null) {
+ options.setToolbarWidgetColor(toolbarWidgetColor);
+ }
+ if (backgroundColor != null) {
+ options.setRootViewBackgroundColor(backgroundColor);
+ }
+ if (activeControlsWidgetColor != null) {
+ options.setActiveControlsWidgetColor(activeControlsWidgetColor);
+ }
+ if (dimmedLayerColor != null) {
+ options.setDimmedLayerColor(dimmedLayerColor);
+ }
+ if (cropFrameColor != null) {
+ options.setCropFrameColor(cropFrameColor);
+ }
+ if (cropGridColor != null) {
+ options.setCropGridColor(cropGridColor);
+ }
+ if (cropFrameStrokeWidth != null) {
+ options.setCropFrameStrokeWidth(cropFrameStrokeWidth);
+ }
+ if (cropGridRowCount != null) {
+ options.setCropGridRowCount(cropGridRowCount);
+ }
+ if (cropGridColumnCount != null) {
+ options.setCropGridColumnCount(cropGridColumnCount);
+ }
+ if (cropGridStrokeWidth != null) {
+ options.setCropGridStrokeWidth(cropGridStrokeWidth);
+ }
+ if (showCropGrid != null) {
+ options.setShowCropGrid(showCropGrid);
+ }
+ if (lockAspectRatio != null) {
+ options.setFreeStyleCropEnabled(!lockAspectRatio);
+ }
+ if (hideBottomControls != null) {
+ options.setHideBottomControls(hideBottomControls);
+ }
+
+ return options;
+ }
+
+
+ private void clearMethodCallAndResult() {
+ pendingResult = null;
+ }
+
+ private int darkenColor(int color) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+ hsv[2] *= 0.8f;
+ return Color.HSVToColor(hsv);
+ }
+
+ private AspectRatio parseAspectRatioName(String name) {
+ if ("square".equals(name)) {
+ return new AspectRatio(null, 1.0f, 1.0f);
+ } else if ("original".equals(name)) {
+ return new AspectRatio(activity.getString(com.yalantis.ucrop.R.string.ucrop_label_original).toUpperCase(),
+ CropImageView.SOURCE_IMAGE_ASPECT_RATIO, 1.0f);
+ } else if ("3x2".equals(name)) {
+ return new AspectRatio(null, 3.0f, 2.0f);
+ } else if ("4x3".equals(name)) {
+ return new AspectRatio(null, 4.0f, 3.0f);
+ } else if ("5x3".equals(name)) {
+ return new AspectRatio(null, 5.0f, 3.0f);
+ } else if ("5x4".equals(name)) {
+ return new AspectRatio(null, 5.0f, 4.0f);
+ } else if ("7x5".equals(name)) {
+ return new AspectRatio(null, 7.0f, 5.0f);
+ } else if ("16x9".equals(name)) {
+ return new AspectRatio(null, 16.0f, 9.0f);
+ } else {
+ return new AspectRatio(activity.getString(com.yalantis.ucrop.R.string.ucrop_label_original).toUpperCase(),
+ CropImageView.SOURCE_IMAGE_ASPECT_RATIO, 1.0f);
+ }
+ }
+}
diff --git a/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/ImageCropperPlugin.java b/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/ImageCropperPlugin.java
new file mode 100644
index 0000000..e7f190f
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/ImageCropperPlugin.java
@@ -0,0 +1,89 @@
+package vn.hunghd.flutter.plugins.imagecropper;
+
+
+import android.app.Activity;
+
+import androidx.appcompat.app.AppCompatDelegate;
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+
+/**
+ * ImageCropperPlugin
+ */
+public class ImageCropperPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware {
+ static {
+ AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
+ }
+
+ private static final String CHANNEL = "plugins.hunghd.vn/image_cropper";
+ private ImageCropperDelegate delegate;
+
+ private ActivityPluginBinding activityPluginBinding;
+
+ private void setupEngine(BinaryMessenger messenger) {
+ MethodChannel channel = new MethodChannel(messenger, CHANNEL);
+ channel.setMethodCallHandler(this);
+ }
+
+ public ImageCropperDelegate setupActivity(Activity activity) {
+ delegate = new ImageCropperDelegate(activity);
+ return delegate;
+ }
+
+ @Override
+ public void onMethodCall(MethodCall call, Result result) {
+
+ if (call.method.equals("cropImage")) {
+ delegate.startCrop(call, result);
+ } else if (call.method.equals("recoverImage")) {
+ delegate.recoverImage(call, result);
+ }
+
+ }
+ //////////////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
+ setupEngine(flutterPluginBinding.getBinaryMessenger());
+ }
+
+ @Override
+ public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
+
+ setupActivity(activityPluginBinding.getActivity());
+ this.activityPluginBinding = activityPluginBinding;
+ activityPluginBinding.addActivityResultListener(delegate);
+ }
+ //////////////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onDetachedFromEngine(FlutterPluginBinding flutterPluginBinding) {
+ // no need to clear channel
+ }
+
+ @Override
+ public void onDetachedFromActivityForConfigChanges() {
+ onDetachedFromActivity();
+ }
+
+ @Override
+ public void onDetachedFromActivity() {
+ activityPluginBinding.removeActivityResultListener(delegate);
+ activityPluginBinding = null;
+ delegate = null;
+ }
+
+ @Override
+ public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
+ onAttachedToActivity(activityPluginBinding);
+ }
+
+
+}
diff --git a/local_packages/image_cropper-5.0.1-patched/ios/Classes/FLTImageCropperPlugin.h b/local_packages/image_cropper-5.0.1-patched/ios/Classes/FLTImageCropperPlugin.h
new file mode 100644
index 0000000..0cee319
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/ios/Classes/FLTImageCropperPlugin.h
@@ -0,0 +1,4 @@
+#import
+
+@interface FLTImageCropperPlugin : NSObject
+@end
diff --git a/local_packages/image_cropper-5.0.1-patched/ios/Classes/FLTImageCropperPlugin.m b/local_packages/image_cropper-5.0.1-patched/ios/Classes/FLTImageCropperPlugin.m
new file mode 100644
index 0000000..fc6b4fa
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/ios/Classes/FLTImageCropperPlugin.m
@@ -0,0 +1,316 @@
+#import "FLTImageCropperPlugin.h"
+#import
+#import
+#import
+#import
+
+@interface FLTImageCropperPlugin()
+@end
+
+@implementation FLTImageCropperPlugin {
+ FlutterResult _result;
+ NSDictionary *_arguments;
+ UIViewController *_viewController;
+ float _compressQuality;
+ NSString *_compressFormat;
+}
++ (void)registerWithRegistrar:(NSObject*)registrar {
+ FlutterMethodChannel* channel = [FlutterMethodChannel
+ methodChannelWithName:@"plugins.hunghd.vn/image_cropper"
+ binaryMessenger:[registrar messenger]];
+ UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController;
+ FLTImageCropperPlugin* instance = [[FLTImageCropperPlugin alloc] initWithViewController:viewController];
+ [registrar addMethodCallDelegate:instance channel:channel];
+}
+
+- (instancetype)initWithViewController:(UIViewController *)viewController {
+ self = [super init];
+ if (self) {
+ _viewController = viewController;
+ }
+ return self;
+}
+
+- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
+ if ([@"cropImage" isEqualToString:call.method]) {
+ _result = result;
+ _arguments = call.arguments;
+ NSString *sourcePath = call.arguments[@"source_path"];
+ NSNumber *ratioX = call.arguments[@"ratio_x"];
+ NSNumber *ratioY = call.arguments[@"ratio_y"];
+ NSString *cropStyle = call.arguments[@"crop_style"];
+ NSArray *aspectRatioPresets = call.arguments[@"aspect_ratio_presets"];
+ NSNumber *compressQuality = call.arguments[@"compress_quality"];
+ NSString *compressFormat = call.arguments[@"compress_format"];
+
+ UIImage *image = [UIImage imageWithContentsOfFile:sourcePath];
+ TOCropViewController *cropViewController;
+
+ if ([@"circle" isEqualToString:cropStyle]) {
+ cropViewController = [[TOCropViewController alloc] initWithCroppingStyle:TOCropViewCroppingStyleCircular image:image];
+ } else {
+ cropViewController = [[TOCropViewController alloc] initWithImage:image];
+ }
+
+ cropViewController.delegate = self;
+
+ if (compressQuality && [compressQuality isKindOfClass:[NSNumber class]]) {
+ _compressQuality = compressQuality.intValue * 1.0f / 100;
+ } else {
+ _compressQuality = 0.9f;
+ }
+ if (compressFormat && [compressFormat isKindOfClass:[NSString class]]) {
+ _compressFormat = compressFormat;
+ } else {
+ _compressFormat = @"jpg";
+ }
+
+ NSMutableArray *allowedAspectRatios = [NSMutableArray new];
+ for (NSString *preset in aspectRatioPresets) {
+ if (preset) {
+ [allowedAspectRatios addObject:@([self parseAspectRatioPresetFromName:preset])];
+ }
+ }
+ cropViewController.allowedAspectRatios = allowedAspectRatios;
+
+ if (ratioX != (id)[NSNull null] && ratioY != (id)[NSNull null]) {
+ cropViewController.customAspectRatio = CGSizeMake([ratioX floatValue], [ratioY floatValue]);
+ cropViewController.resetAspectRatioEnabled = NO;
+ cropViewController.aspectRatioPickerButtonHidden = YES;
+ cropViewController.aspectRatioLockDimensionSwapEnabled = YES;
+ cropViewController.aspectRatioLockEnabled = YES;
+ }
+
+ [self setupUiCustomizedOptions:call.arguments forViewController:cropViewController];
+
+ UIWindow *window = [UIApplication sharedApplication].delegate.window;
+ if (!window && @available(iOS 13.0, *)) {
+ for (UIWindowScene* scene in [UIApplication sharedApplication].connectedScenes) {
+ if (scene.activationState == UISceneActivationStateForegroundActive) {
+ for (UIWindow *w in scene.windows) {
+ if (w.isKeyWindow) {
+ window = w;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ [window.rootViewController presentViewController:cropViewController animated:YES completion:nil];
+ } else {
+ result(FlutterMethodNotImplemented);
+ }
+}
+
+- (void)setupUiCustomizedOptions:(id)options forViewController:(TOCropViewController*)controller {
+ NSNumber *minimumAspectRatio = options[@"ios.minimum_aspect_ratio"];
+ NSNumber *rectX = options[@"ios.rect_x"];
+ NSNumber *rectY = options[@"ios.rect_y"];
+ NSNumber *rectWidth = options[@"ios.rect_width"];
+ NSNumber *rectHeight = options[@"ios.rect_height"];
+ NSNumber *showActivitySheetOnDone = options[@"ios.show_activity_sheet_on_done"];
+ NSNumber *showCancelConfirmationDialog = options[@"ios.show_cancel_confirmation_dialog"];
+ NSNumber *rotateClockwiseButtonHidden = options[@"ios.rotate_clockwise_button_hidden"];
+ NSNumber *hidesNavigationBar = options[@"ios.hides_navigation_bar"];
+ NSNumber *rotateButtonHidden = options[@"ios.rotate_button_hidden"];
+ NSNumber *resetButtonHidden = options[@"ios.reset_button_hidden"];
+ NSNumber *aspectRatioPickerButtonHidden = options[@"ios.aspect_ratio_picker_button_hidden"];
+ NSNumber *resetAspectRatioEnabled = options[@"ios.reset_aspect_ratio_enabled"];
+ NSNumber *aspectRatioLockDimensionSwapEnabled = options[@"ios.aspect_ratio_lock_dimension_swap_enabled"];
+ NSNumber *aspectRatioLockEnabled = options[@"ios.aspect_ratio_lock_enabled"];
+ NSString *title = options[@"ios.title"];
+ NSString *doneButtonTitle = options[@"ios.done_button_title"];
+ NSString *cancelButtonTitle = options[@"ios.cancel_button_title"];
+
+ if (minimumAspectRatio && [minimumAspectRatio isKindOfClass:[NSNumber class]]) {
+ controller.minimumAspectRatio = minimumAspectRatio.floatValue;
+ }
+ if (rectX && [rectX isKindOfClass:[NSNumber class]]
+ && rectY && [rectY isKindOfClass:[NSNumber class]]
+ && rectWidth && [rectWidth isKindOfClass:[NSNumber class]]
+ && rectHeight && [rectHeight isKindOfClass:[NSNumber class]]) {
+ controller.imageCropFrame = CGRectMake(rectX.floatValue, rectY.floatValue, rectWidth.floatValue, rectHeight.floatValue);
+ }
+ if (showActivitySheetOnDone && [showActivitySheetOnDone isKindOfClass:[NSNumber class]]) {
+ controller.showActivitySheetOnDone = showActivitySheetOnDone.boolValue;
+ }
+ if (showCancelConfirmationDialog && [showCancelConfirmationDialog isKindOfClass:[NSNumber class]]) {
+ controller.showCancelConfirmationDialog = showCancelConfirmationDialog.boolValue;
+ }
+ if (rotateClockwiseButtonHidden && [rotateClockwiseButtonHidden isKindOfClass:[NSNumber class]]) {
+ controller.rotateClockwiseButtonHidden = rotateClockwiseButtonHidden.boolValue;
+ }
+ if (hidesNavigationBar && [hidesNavigationBar isKindOfClass:[NSNumber class]]) {
+ controller.hidesNavigationBar = hidesNavigationBar.boolValue;
+ }
+ if (rotateButtonHidden && [rotateButtonHidden isKindOfClass:[NSNumber class]]) {
+ controller.rotateButtonsHidden = rotateButtonHidden.boolValue;
+ }
+ if (resetButtonHidden && [resetButtonHidden isKindOfClass:[NSNumber class]]) {
+ controller.resetButtonHidden = resetButtonHidden.boolValue;
+ }
+ if (aspectRatioPickerButtonHidden && [aspectRatioPickerButtonHidden isKindOfClass:[NSNumber class]]) {
+ controller.aspectRatioPickerButtonHidden = aspectRatioPickerButtonHidden.boolValue;
+ }
+ if (resetAspectRatioEnabled && [resetAspectRatioEnabled isKindOfClass:[NSNumber class]]) {
+ controller.resetAspectRatioEnabled = resetAspectRatioEnabled.boolValue;
+ }
+ if (aspectRatioLockDimensionSwapEnabled && [aspectRatioLockDimensionSwapEnabled isKindOfClass:[NSNumber class]]) {
+ controller.aspectRatioLockDimensionSwapEnabled = aspectRatioLockDimensionSwapEnabled.boolValue;
+ }
+ if (aspectRatioLockEnabled && [aspectRatioLockEnabled isKindOfClass:[NSNumber class]]) {
+ controller.aspectRatioLockEnabled = aspectRatioLockEnabled.boolValue;
+ }
+ if (title && [title isKindOfClass:[NSString class]]) {
+ controller.title = title;
+ }
+ if (doneButtonTitle && [doneButtonTitle isKindOfClass:[NSString class]]) {
+ controller.doneButtonTitle = doneButtonTitle;
+ }
+ if (cancelButtonTitle && [cancelButtonTitle isKindOfClass:[NSString class]]) {
+ controller.cancelButtonTitle = cancelButtonTitle;
+ }
+}
+
+- (TOCropViewControllerAspectRatioPreset)parseAspectRatioPresetFromName:(NSString*)name {
+ if ([@"square" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPresetSquare;
+ } else if ([@"original" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPresetOriginal;
+ } else if ([@"3x2" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPreset3x2;
+ } else if ([@"4x3" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPreset4x3;
+ } else if ([@"5x3" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPreset5x3;
+ } else if ([@"5x4" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPreset5x4;
+ } else if ([@"7x5" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPreset7x5;
+ } else if ([@"16x9" isEqualToString:name]) {
+ return TOCropViewControllerAspectRatioPreset16x9;
+ } else {
+ return TOCropViewControllerAspectRatioPresetOriginal;
+ }
+}
+
+# pragma TOCropViewControllerDelegate
+
+- (void)cropViewController:(TOCropViewController *)cropViewController didCropToImage:(UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle
+{
+ image = [self normalizedImage:image];
+
+ NSNumber *maxWidth = [_arguments objectForKey:@"max_width"];
+ NSNumber *maxHeight = [_arguments objectForKey:@"max_height"];
+
+ if (maxWidth != (id)[NSNull null] && maxHeight != (id)[NSNull null]) {
+ image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight];
+ }
+
+ NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
+
+ NSData *data;
+ NSString *tmpFile;
+
+ if ([@"png" isEqualToString:_compressFormat]) {
+ data = UIImagePNGRepresentation(image);
+ tmpFile = [NSString stringWithFormat:@"image_cropper_%@.png", guid];
+ } else {
+ data = UIImageJPEGRepresentation(image, _compressQuality);
+ tmpFile = [NSString stringWithFormat:@"image_cropper_%@.jpg", guid];
+ }
+
+ NSString *tmpDirectory = NSTemporaryDirectory();
+ NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile];
+
+ if (_result) {
+ if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:data attributes:nil]) {
+ _result(tmpPath);
+ } else {
+ _result([FlutterError errorWithCode:@"create_error"
+ message:@"Temporary file could not be created"
+ details:nil]);
+ }
+
+ [cropViewController dismissViewControllerAnimated:YES completion:nil];
+
+ _result = nil;
+ _arguments = nil;
+ }
+}
+
+- (void)cropViewController:(TOCropViewController *)cropViewController didFinishCancelled:(BOOL)cancelled {
+ [cropViewController dismissViewControllerAnimated:YES completion:nil];
+ _result(nil);
+
+ _result = nil;
+ _arguments = nil;
+}
+
+// The way we save images to the tmp dir currently throws away all EXIF data
+// (including the orientation of the image). That means, pics taken in portrait
+// will not be orientated correctly as is. To avoid that, we rotate the actual
+// image data.
+// TODO(goderbauer): investigate how to preserve EXIF data.
+- (UIImage *)normalizedImage:(UIImage *)image {
+ if (image.imageOrientation == UIImageOrientationUp) return image;
+
+ UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
+ [image drawInRect:(CGRect){0, 0, image.size}];
+ UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return normalizedImage;
+}
+
+- (UIImage *)scaledImage:(UIImage *)image
+ maxWidth:(NSNumber *)maxWidth
+ maxHeight:(NSNumber *)maxHeight {
+ double originalWidth = image.size.width;
+ double originalHeight = image.size.height;
+
+ bool hasMaxWidth = maxWidth != (id)[NSNull null];
+ bool hasMaxHeight = maxHeight != (id)[NSNull null];
+
+ double width = hasMaxWidth ? MIN([maxWidth doubleValue], originalWidth) : originalWidth;
+ double height = hasMaxHeight ? MIN([maxHeight doubleValue], originalHeight) : originalHeight;
+
+ bool shouldDownscaleWidth = hasMaxWidth && [maxWidth doubleValue] < originalWidth;
+ bool shouldDownscaleHeight = hasMaxHeight && [maxHeight doubleValue] < originalHeight;
+ bool shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight;
+
+ if (shouldDownscale) {
+ double downscaledWidth = (height / originalHeight) * originalWidth;
+ double downscaledHeight = (width / originalWidth) * originalHeight;
+
+ if (width < height) {
+ if (!hasMaxWidth) {
+ width = downscaledWidth;
+ } else {
+ height = downscaledHeight;
+ }
+ } else if (height < width) {
+ if (!hasMaxHeight) {
+ height = downscaledHeight;
+ } else {
+ width = downscaledWidth;
+ }
+ } else {
+ if (originalWidth < originalHeight) {
+ width = downscaledWidth;
+ } else if (originalHeight < originalWidth) {
+ height = downscaledHeight;
+ }
+ }
+ }
+
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0);
+ [image drawInRect:CGRectMake(0, 0, width, height)];
+
+ UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ return scaledImage;
+}
+
+@end
diff --git a/local_packages/image_cropper-5.0.1-patched/ios/image_cropper.podspec b/local_packages/image_cropper-5.0.1-patched/ios/image_cropper.podspec
new file mode 100644
index 0000000..d67059b
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/ios/image_cropper.podspec
@@ -0,0 +1,22 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
+#
+Pod::Spec.new do |s|
+ s.name = 'image_cropper'
+ s.version = '0.0.4'
+ s.summary = 'A Flutter plugin supports cropping images'
+ s.description = <<-DESC
+A Flutter plugin supports cropping images
+ DESC
+ s.homepage = 'https://github.com/hnvn/flutter_image_cropper'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'HungHD' => 'hunghd.yb@gmail.com' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+ s.public_header_files = 'Classes/**/*.h'
+ s.dependency 'Flutter'
+ s.dependency 'TOCropViewController', '~> 2.6.1'
+
+ s.ios.deployment_target = '11.0'
+end
+
diff --git a/local_packages/image_cropper-5.0.1-patched/lib/image_cropper.dart b/local_packages/image_cropper-5.0.1-patched/lib/image_cropper.dart
new file mode 100644
index 0000000..413e3c9
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/lib/image_cropper.dart
@@ -0,0 +1,32 @@
+///
+/// * author: Hung Duy Ha (hunghd)
+/// * email: hunghd.yb@gmail.com
+///
+/// A plugin provides capability of manipulating an image including rotating
+/// and cropping.
+///
+/// Note that: this plugin is based on different native libraries depending on
+/// Android or iOS platform, so it shows different UI look and feel between
+/// those platforms.
+///
+
+export 'package:image_cropper_platform_interface/image_cropper_platform_interface.dart'
+ show
+ CropAspectRatioPreset,
+ CropStyle,
+ ImageCompressFormat,
+ CropAspectRatio,
+ CroppedFile,
+ PlatformUiSettings,
+ WebTranslations,
+ AndroidUiSettings,
+ IOSUiSettings,
+ CropperDialogBuilder,
+ CropperRouteBuilder,
+ CropperPresentStyle,
+ WebUiSettings,
+ CroppieViewPort,
+ CroppieBoundary,
+ RotationAngle;
+
+export 'src/cropper.dart';
diff --git a/local_packages/image_cropper-5.0.1-patched/lib/src/cropper.dart b/local_packages/image_cropper-5.0.1-patched/lib/src/cropper.dart
new file mode 100644
index 0000000..b6b953a
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/lib/src/cropper.dart
@@ -0,0 +1,142 @@
+// Copyright 2013, the Dart project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:image_cropper_platform_interface/image_cropper_platform_interface.dart';
+
+///
+/// A convenient class wraps all api functions of **ImageCropper** plugin
+///
+class ImageCropper {
+ static ImageCropperPlatform get platform => ImageCropperPlatform.instance;
+
+ ///
+ /// Launch cropper UI for an image.
+ ///
+ ///
+ /// **parameters:**
+ ///
+ /// * sourcePath: the absolute path of an image file.
+ ///
+ /// * maxWidth: maximum cropped image width. Note: this field is ignored on Web.
+ ///
+ /// * maxHeight: maximum cropped image height. Note: this field is ignored on Web
+ ///
+ /// * aspectRatio: controls the aspect ratio of crop bounds. If this values is set,
+ /// the cropper is locked and user can't change the aspect ratio of crop bounds.
+ /// Note: this field is ignored on Web
+ ///
+ /// * aspectRatioPresets: controls the list of aspect ratios in the crop menu view.
+ /// In Android, you can set the initialized aspect ratio when starting the cropper
+ /// by setting the value of [AndroidUiSettings.initAspectRatio]. Default is a list of
+ /// [CropAspectRatioPreset.original], [CropAspectRatioPreset.square],
+ /// [CropAspectRatioPreset.ratio3x2], [CropAspectRatioPreset.ratio4x3] and
+ /// [CropAspectRatioPreset.ratio16x9].
+ /// Note: this field is ignored on Web
+ ///
+ /// * cropStyle: controls the style of crop bounds, it can be rectangle or
+ /// circle style (default is [CropStyle.rectangle]).
+ /// Note: on Web, this field can be overrided by [WebUiSettings.viewPort.type]
+ ///
+ /// * compressFormat: the format of result image, png or jpg (default is [ImageCompressFormat.jpg])
+ ///
+ /// * compressQuality: the value [0 - 100] to control the quality of image compression
+ ///
+ /// * uiSettings: controls UI customization on specific platform (android, ios, web,...)
+ ///
+ /// See:
+ /// * [AndroidUiSettings] controls UI customization for Android
+ /// * [IOSUiSettings] controls UI customization for iOS
+ ///
+ /// **return:**
+ ///
+ /// A result file of the cropped image.
+ ///
+ /// **Note:**
+ ///
+ /// * The result file is saved in NSTemporaryDirectory on iOS and application Cache directory
+ /// on Android, so it can be lost later, you are responsible for storing it somewhere
+ /// permanent (if needed).
+ ///
+ /// * The implementation on Web is much different compared to the implementation on mobile app.
+ /// It causes some configuration fields not working (maxWidth, maxHeight, aspectRatio,
+ /// aspectRatioPresets) and [WebUiSettings] is required for Web.
+ ///
+ Future cropImage({
+ required String sourcePath,
+ int? maxWidth,
+ int? maxHeight,
+ CropAspectRatio? aspectRatio,
+ List aspectRatioPresets = const [
+ CropAspectRatioPreset.original,
+ CropAspectRatioPreset.square,
+ CropAspectRatioPreset.ratio3x2,
+ CropAspectRatioPreset.ratio4x3,
+ CropAspectRatioPreset.ratio16x9
+ ],
+ CropStyle cropStyle = CropStyle.rectangle,
+ ImageCompressFormat compressFormat = ImageCompressFormat.jpg,
+ int compressQuality = 90,
+ List? uiSettings,
+ }) {
+ return platform.cropImage(
+ sourcePath: sourcePath,
+ maxWidth: maxWidth,
+ maxHeight: maxHeight,
+ aspectRatio: aspectRatio,
+ aspectRatioPresets: aspectRatioPresets,
+ cropStyle: cropStyle,
+ compressFormat: compressFormat,
+ compressQuality: compressQuality,
+ uiSettings: uiSettings,
+ );
+ }
+
+ ///
+ /// Retrieve cropped image lost due to activity termination (Android only).
+ /// This method works similarly to [retrieveLostData] method from [image_picker]
+ /// library. Unlike [retrieveLostData], does not throw an error on other platforms,
+ /// but returns null result.
+ ///
+ /// [recoverImage] as (well as [retrieveLostData]) will return value on any
+ /// call after a successful [cropImage], so you can potentially get unexpected
+ /// result when using [ImageCropper] in different layout. Recommended usage comes down to
+ /// this:
+ ///
+ /// ```dart
+ /// void crop() async {
+ /// final cropper = ImageCropper();
+ /// final croppedFile = await cropper.cropImage(/* your parameters */);
+ /// // At this point we know that the main activity did survive and we can
+ /// // discard the cached value
+ /// await cropper.recoverImage();
+ /// // process croppedFile value
+ /// }
+ ///
+ /// @override
+ /// void initState() {
+ /// _getLostData();
+ /// super.initState();
+ /// }
+ ///
+ /// void _getLostData() async {
+ /// final recoveredCroppedImage = await ImageCropper().recoverImage();
+ /// if (recoveredCroppedImage != null) {
+ /// // process recoveredCroppedImage value
+ /// }
+ /// }
+ /// ```
+ ///
+ /// **return:**
+ ///
+ /// A result file of the cropped image.
+ ///
+ /// See also:
+ /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html)
+ ///
+ Future recoverImage() {
+ return platform.recoverImage();
+ }
+}
diff --git a/local_packages/image_cropper-5.0.1-patched/pubspec.lock b/local_packages/image_cropper-5.0.1-patched/pubspec.lock
new file mode 100644
index 0000000..a56f17c
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/pubspec.lock
@@ -0,0 +1,258 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.13.1"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.1.2"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.4.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.1.2"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.19.1"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.3.3"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_web_plugins:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.6.0"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "4.1.2"
+ image_cropper_for_web:
+ dependency: "direct main"
+ description:
+ name: image_cropper_for_web
+ sha256: "865d798b5c9d826f1185b32e5d0018c4183ddb77b7b82a931e1a06aa3b74974e"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.0.0"
+ image_cropper_platform_interface:
+ dependency: "direct main"
+ description:
+ name: image_cropper_platform_interface
+ sha256: ee160d686422272aa306125f3b6fb1c1894d9b87a5e20ed33fa008e7285da11e
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "5.0.0"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.6.7"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "11.0.2"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.0.10"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.0.2"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.12.19"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.13.0"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.17.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.9.1"
+ plugin_platform_interface:
+ dependency: "direct dev"
+ description:
+ name: plugin_platform_interface
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.1.8"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.10.2"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.12.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.1.4"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.4.1"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.2.2"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.7.10"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.4.0"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.2.0"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "15.0.2"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.1.1"
+sdks:
+ dart: ">=3.9.0-0 <4.0.0"
+ flutter: ">=3.18.0-18.0.pre.54"
diff --git a/local_packages/image_cropper-5.0.1-patched/pubspec.yaml b/local_packages/image_cropper-5.0.1-patched/pubspec.yaml
new file mode 100644
index 0000000..2554eb5
--- /dev/null
+++ b/local_packages/image_cropper-5.0.1-patched/pubspec.yaml
@@ -0,0 +1,30 @@
+name: image_cropper
+description: A Flutter plugin for Android, iOS and Web supports cropping images
+version: 5.0.1
+homepage: https://github.com/hnvn/flutter_image_cropper
+
+environment:
+ sdk: '>=2.18.0 <4.0.0'
+ flutter: '>=3.3.0'
+
+flutter:
+ plugin:
+ platforms:
+ android:
+ package: vn.hunghd.flutter.plugins.imagecropper
+ pluginClass: ImageCropperPlugin
+ ios:
+ pluginClass: FLTImageCropperPlugin
+ web:
+ default_package: image_cropper_for_web
+
+dependencies:
+ flutter:
+ sdk: flutter
+ image_cropper_platform_interface: ^5.0.0
+ image_cropper_for_web: ^3.0.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ plugin_platform_interface: ^2.0.0
\ No newline at end of file
diff --git a/local_packages/on_audio_query-2.9.0/example/android/local.properties b/local_packages/on_audio_query-2.9.0/example/android/local.properties
index 28e8fd6..a7100ac 100644
--- a/local_packages/on_audio_query-2.9.0/example/android/local.properties
+++ b/local_packages/on_audio_query-2.9.0/example/android/local.properties
@@ -1,2 +1,2 @@
-sdk.dir=D:\\AndroidSDK
-flutter.sdk=D:\\FlutterSDK\\flutter_windows_3.29.3-stable\\flutter
\ No newline at end of file
+sdk.dir=/Users/nigger/Library/Android/sdk
+flutter.sdk=/Users/nigger/Desktop/develop/flutter
\ No newline at end of file
diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/ephemeral/flutter_lldb_helper.py
new file mode 100644
index 0000000..a88caf9
--- /dev/null
+++ b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/ephemeral/flutter_lldb_helper.py
@@ -0,0 +1,32 @@
+#
+# Generated file, do not edit.
+#
+
+import lldb
+
+def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
+ """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
+ base = frame.register["x0"].GetValueAsAddress()
+ page_len = frame.register["x1"].GetValueAsUnsigned()
+
+ # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
+ # first page to see if handled it correctly. This makes diagnosing
+ # misconfiguration (e.g. missing breakpoint) easier.
+ data = bytearray(page_len)
+ data[0:8] = b'IHELPED!'
+
+ error = lldb.SBError()
+ frame.GetThread().GetProcess().WriteMemory(base, data, error)
+ if not error.Success():
+ print(f'Failed to write into {base}[+{page_len}]', error)
+ return
+
+def __lldb_init_module(debugger: lldb.SBDebugger, _):
+ target = debugger.GetDummyTarget()
+ # Caveat: must use BreakpointCreateByRegEx here and not
+ # BreakpointCreateByName. For some reasons callback function does not
+ # get carried over from dummy target for the later.
+ bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
+ bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
+ bp.SetAutoContinue(True)
+ print("-- LLDB integration loaded --")
diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Flutter/ephemeral/flutter_lldbinit b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/ephemeral/flutter_lldbinit
new file mode 100644
index 0000000..e3ba6fb
--- /dev/null
+++ b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/ephemeral/flutter_lldbinit
@@ -0,0 +1,5 @@
+#
+# Generated file, do not edit.
+#
+
+command script import --relative-to-command-file flutter_lldb_helper.py
diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Podfile b/local_packages/on_audio_query-2.9.0/example/ios/Podfile
new file mode 100644
index 0000000..620e46e
--- /dev/null
+++ b/local_packages/on_audio_query-2.9.0/example/ios/Podfile
@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/GeneratedPluginRegistrant.h b/local_packages/on_audio_query-2.9.0/example/ios/Runner/GeneratedPluginRegistrant.h
new file mode 100644
index 0000000..7a89092
--- /dev/null
+++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/GeneratedPluginRegistrant.h
@@ -0,0 +1,19 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GeneratedPluginRegistrant_h
+#define GeneratedPluginRegistrant_h
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GeneratedPluginRegistrant : NSObject
++ (void)registerWithRegistry:(NSObject*)registry;
+@end
+
+NS_ASSUME_NONNULL_END
+#endif /* GeneratedPluginRegistrant_h */
diff --git a/local_packages/on_audio_query-2.9.0/example/pubspec.lock b/local_packages/on_audio_query-2.9.0/example/pubspec.lock
index c6545dc..1ba57fe 100644
--- a/local_packages/on_audio_query-2.9.0/example/pubspec.lock
+++ b/local_packages/on_audio_query-2.9.0/example/pubspec.lock
@@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.flutter-io.cn"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
clock:
dependency: transitive
description:
@@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: fake_async
- sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "1.3.2"
+ version: "1.3.3"
file:
dependency: transitive
description:
@@ -115,26 +115,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
- sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "10.0.8"
+ version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
- sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "3.0.9"
+ version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
- sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "3.0.1"
+ version: "3.0.2"
lints:
dependency: transitive
description:
@@ -147,26 +147,26 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.12.17"
+ version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "1.16.0"
+ version: "1.17.0"
on_audio_query:
dependency: "direct main"
description:
@@ -177,10 +177,9 @@ packages:
on_audio_query_android:
dependency: transitive
description:
- name: on_audio_query_android
- sha256: c0b946011ee7aa8ec76b21837f0cb0007120ac291202ed6c6fb4c498ba2ff6fe
- url: "https://pub.flutter-io.cn"
- source: hosted
+ path: "../../on_audio_query_android-1.1.0"
+ relative: true
+ source: path
version: "1.1.0"
on_audio_query_ios:
dependency: transitive
@@ -295,18 +294,18 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.7.4"
+ version: "0.7.10"
vector_math:
dependency: transitive
description:
name: vector_math
- sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.flutter-io.cn"
source: hosted
- version: "2.1.4"
+ version: "2.2.0"
vm_service:
dependency: transitive
description:
@@ -324,5 +323,5 @@ packages:
source: hosted
version: "3.0.4"
sdks:
- dart: ">=3.7.0-0 <4.0.0"
+ dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
diff --git a/local_packages/on_audio_query-2.9.0/pubspec.lock b/local_packages/on_audio_query-2.9.0/pubspec.lock
index a0cb754..01716f7 100644
--- a/local_packages/on_audio_query-2.9.0/pubspec.lock
+++ b/local_packages/on_audio_query-2.9.0/pubspec.lock
@@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.flutter-io.cn"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
clock:
dependency: transitive
description:
@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: fake_async
- sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "1.3.2"
+ version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
@@ -84,26 +84,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
- sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "10.0.8"
+ version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
- sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "3.0.9"
+ version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
- sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "3.0.1"
+ version: "3.0.2"
lints:
dependency: transitive
description:
@@ -116,33 +116,32 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.12.17"
+ version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "1.16.0"
+ version: "1.17.0"
on_audio_query_android:
dependency: "direct main"
description:
- name: on_audio_query_android
- sha256: c0b946011ee7aa8ec76b21837f0cb0007120ac291202ed6c6fb4c498ba2ff6fe
- url: "https://pub.flutter-io.cn"
- source: hosted
+ path: "../on_audio_query_android-1.1.0"
+ relative: true
+ source: path
version: "1.1.0"
on_audio_query_ios:
dependency: "direct main"
@@ -233,18 +232,18 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.7.4"
+ version: "0.7.10"
vector_math:
dependency: transitive
description:
name: vector_math
- sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.flutter-io.cn"
source: hosted
- version: "2.1.4"
+ version: "2.2.0"
vm_service:
dependency: transitive
description:
@@ -254,5 +253,5 @@ packages:
source: hosted
version: "14.3.1"
sdks:
- dart: ">=3.7.0-0 <4.0.0"
+ dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
diff --git a/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift
index bdf2b8d..226c71d 100644
--- a/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift
+++ b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift
@@ -1,723 +1,106 @@
-import Flutter
-import UIKit
-import QGVAPlayer
-import Darwin.Mach
-
-public class VapFlutterView: NSObject, FlutterPlatformView {
- private let _view: UIView
- private let channel: FlutterMethodChannel
- private var vapView: QGVAPWrapView?
- private var repeatCount: Int = 0
- private var playResult: FlutterResult?
- private var vapTagContents: [String: [String: Any]] = [:]
-
-
- init(
- context: CGRect,
- params: [String: Any]?,
- messenger: FlutterBinaryMessenger,
- id: Int64
- ) {
- _view = UIView(frame: context)
- channel = FlutterMethodChannel(name: "vap_view_\(id)", binaryMessenger: messenger)
-
- super.init()
-
- // Initialize VAP view with proper configuration
- vapView = QGVAPWrapView(frame: context)
-
- vapView?.center = _view.center
- // Set scaleType from params (matching Kotlin logic)
- if let scaleType = params?["scaleType"] as? String {
- switch scaleType {
- case "fitCenter":
- vapView?.contentMode = .aspectFit
- break
- case "centerCrop":
- vapView?.contentMode = .aspectFill
- break
- case "fitXY":
- vapView?.contentMode = .scaleToFill
- break
- default:
- vapView?.contentMode = .aspectFit
- break
- }
- } else {
- // Default scale type (matching Kotlin's default)
- vapView?.contentMode = .aspectFit
- }
-
- // Configure background color to prevent visual glitches
- vapView?.backgroundColor = UIColor.clear
-
- _view.backgroundColor = UIColor.clear
-
- channel.setMethodCallHandler(onMethodCall)
-
- // Add vapView to container
- if let vapView = vapView {
- vapView.translatesAutoresizingMaskIntoConstraints = false
- _view.addSubview(vapView)
-
- // Set up auto layout constraints
- NSLayoutConstraint.activate([
- vapView.topAnchor.constraint(equalTo: _view.topAnchor),
- vapView.leadingAnchor.constraint(equalTo: _view.leadingAnchor),
- vapView.trailingAnchor.constraint(equalTo: _view.trailingAnchor),
- vapView.bottomAnchor.constraint(equalTo: _view.bottomAnchor)
- ])
- }
-
- // Initial playback if filePath or assetName is provided (matching Kotlin)
- if let filePath = params?["filePath"] as? String {
- playFile(filePath)
- }
- if let assetName = params?["assetName"] as? String {
- playAsset(assetName)
- }
- if let loop = params?["loop"] as? Int {
- setLoop(loop)
- }
- if let mute = params?["mute"] as? Bool {
- setMute(mute)
- }
-
- // Store tag contents as Maps
- if let tagContents = params?["vapTagContents"] as? [String: [String: Any]] {
- for (tag, content) in tagContents {
- vapTagContents[tag] = content
- NSLog("Stored tag content for tag: \(tag), contentType: \(content["contentType"] ?? "unknown")")
- }
- }
- }
-
- public func view() -> UIView {
- return _view
- }
-
- private func reset() {
- // Ensure cleanup happens on main thread
- DispatchQueue.main.async { [weak self] in
- guard let self = self else { return }
-
- clearVapTagContents()
-
- // Stop VAP playback and clean up resources
- self.vapView?.stopHWDMP4()
-
- // Give a moment for VideoToolbox to clean up
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- // Remove all subviews
- self._view.subviews.forEach { $0.removeFromSuperview() }
-
- // Clear the VAP view reference
- self.vapView = nil
-
- // Force garbage collection hint
- if #available(iOS 13.0, *) {
- // Modern iOS versions handle this automatically
- } else {
- // For older iOS versions, hint at memory cleanup
- DispatchQueue.global(qos: .background).async {
- // Trigger background cleanup
- }
- }
- }
- }
- }
-
- private func dispose() {
- reset()
- channel.setMethodCallHandler(nil)
- }
-
- private func onMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- switch call.method {
- case "dispose":
- dispose()
- result(nil)
-
- case "playFile":
- if let args = call.arguments as? [String: Any],
- let filePath = args["filePath"] as? String {
- playFile(filePath, result)
- }
-
- case "playAsset":
- if let args = call.arguments as? [String: Any],
- let assetName = args["assetName"] as? String {
- playAsset(assetName, result)
- }
-
- case "stop":
- stop()
- result(nil)
-
- case "setLoop":
- if let args = call.arguments as? [String: Any],
- let loop = args["loop"] as? Int {
- setLoop(loop)
- } else {
- setLoop(0)
- }
- result(nil)
-
- case "setMute":
- if let args = call.arguments as? [String: Any],
- let mute = args["mute"] as? Bool {
- setMute(mute)
- } else {
- setMute(false)
- }
- result(nil)
-
- case "setScaleType":
- if let args = call.arguments as? [String: Any],
- let scaleType = args["scaleType"] as? String {
- setScaleType(scaleType)
- }
- result(nil)
-
- case "setVapTagContent":
- if let args = call.arguments as? [String: Any],
- let tag = args["tag"] as? String,
- let contentMap = args["content"] as? [String: Any] {
- setVapTagContent(tag: tag, contentMap: contentMap)
- }
- result(nil)
-
- case "setVapTagContents":
- if let args = call.arguments as? [String: Any],
- let contents = args["contents"] as? [String: [String: Any]] {
- setVapTagContents(contents)
- }
- result(nil)
-
- case "getVapTagContent":
- if let args = call.arguments as? [String: Any],
- let tag = args["tag"] as? String {
- result(getVapTagContent(tag: tag))
- } else {
- result(nil)
- }
-
- case "getAllVapTagContents":
- result(vapTagContents)
-
- case "clearVapTagContents":
- clearVapTagContents()
- result(nil)
-
- default:
- result(FlutterMethodNotImplemented)
- }
- }
-
- private func playFile(_ filePath: String, _ result: FlutterResult? = nil) {
- // Ensure we're on the main thread for UI operations
- self.playResult = result
- DispatchQueue.main.async { [weak self] in
- guard let self = self, let vapView = self.vapView else { return }
-
- // Stop any existing playback first
- vapView.stopHWDMP4()
-
- // Configure VAP player with better error handling
- do {
- guard FileManager.default.fileExists(atPath: filePath) else {
- self.sendFailedEvent(errorCode: -1, errorType: "FILE_NOT_FOUND", errorMsg: "VAP file not found: \(filePath)")
- return
- }
-
- // Check file size (VAP files shouldn't be too large for memory)
- let fileAttributes = try FileManager.default.attributesOfItem(atPath: filePath)
- if let fileSize = fileAttributes[.size] as? NSNumber {
- let fileSizeInMB = fileSize.doubleValue / (1024 * 1024)
- // Reject extremely large files that will definitely cause issues
- if fileSizeInMB > 100 {
- self.sendFailedEvent(errorCode: -1006, errorType: "FILE_TOO_LARGE", errorMsg: "VAP file too large (\(fileSizeInMB) MB), maximum size is 100MB")
- playError(-1006, "FILE_TOO_LARGE", "VAP file too large (\(fileSizeInMB) MB), maximum size is 100MB")
- return
- }
- }
- // Start playback with delegate
- vapView.playHWDMP4(filePath, repeatCount: self.repeatCount, delegate: self)
-
-
- } catch {
- self.sendFailedEvent(errorCode: -2, errorType: "FILE_PLAYBACK_ERROR", errorMsg: "Failed to play VAP file: \(error.localizedDescription)")
- }
- }
- }
-
- private func playAsset(_ assetName: String, _ result: FlutterResult? = nil) {
- let key = FlutterDartProject.lookupKey(forAsset: assetName)
- if let bundlePath = Bundle.main.path(forResource: key, ofType: nil) {
- playFile(bundlePath)
- } else {
- sendFailedEvent(errorCode: -1, errorType: "FILE_NOT_FOUND", errorMsg: "Asset not found: \(assetName)")
- }
- }
-
- private func stop() {
- vapView?.stopHWDMP4()
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onVideoDestroy", arguments: nil)
- }
- }
-
- private func setLoop(_ loop: Int) {
- repeatCount = loop
- }
-
- private func setMute(_ mute: Bool) {
- vapView?.setMute(mute)
- }
-
- private func setScaleType(_ scaleType: String) {
- switch scaleType {
- case "fitCenter":
- vapView?.contentMode = .aspectFit
- break
- case "centerCrop":
- vapView?.contentMode = .aspectFill
- break
- case "fitXY":
- vapView?.contentMode = .scaleToFill
- break
- default:
- vapView?.contentMode = .aspectFit
- break
- }
- }
-
- // MARK: - VAP Tag Content Management
- private func setVapTagContent(tag: String, contentMap: [String: Any]) {
- vapTagContents[tag] = contentMap
- }
-
- private func setVapTagContents(_ contents: [String: [String: Any]]) {
- vapTagContents.merge(contents) { (_, new) in new }
- }
-
- private func getVapTagContent(tag: String) -> String? {
- guard let contentMap = vapTagContents[tag],
- let contentValue = contentMap["contentValue"] as? String else {
- return nil
- }
- return contentValue
- }
-
- private func clearVapTagContents() {
- vapTagContents.removeAll()
- }
-
- // MARK: - Event Handling
- private func sendFailedEvent(errorCode: Int, errorType: String, errorMsg: String?) {
- let args: [String: Any?] = [
- "errorCode": errorCode,
- "errorType": errorType,
- "errorMsg": errorMsg
- ]
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onFailed", arguments: args)
- }
- }
-
- private func sendVideoCompleteEvent() {
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onVideoComplete", arguments: nil)
- }
- }
-
- private func sendVideoRenderEvent(frameIndex: Int, config: [String: Any]?) {
- let args: [String: Any?] = [
- "frameIndex": frameIndex,
- "config": config
- ]
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onVideoRender", arguments: args)
- }
- }
-
- private func sendVideoConfigReadyEvent(config: [String: Any]) {
- let args = ["config": config]
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onVideoConfigReady", arguments: args)
- }
- }
-
- // MARK: - Objective-C 兼容性桥接方法
- @objc public func vapWrap_viewDidFinishPlayMP4(_ totalFrameCount: Int, view: UIView) {
- // 注意:参数 `view` 是普通的 UIView,不是 QGVAPWrapView
- // 我们直接调用已有方法,并使用 self.vapView 作为参数
- if let vapView = self.vapView {
- self.viewDidFinishPlayMP4(totalFrameCount, view: vapView)
- }
- }
-
- @objc public func vapWrap_viewDidStartPlayMP4(_ totalFrameCount: Int, view: UIView) {
- // 你原来的 viewDidStartPlayMP4 方法只接受一个 QGVAPWrapView 参数
- // 忽略 totalFrameCount 参数,只传递 vapView
- if let vapView = self.vapView {
- self.viewDidStartPlayMP4(vapView)
- }
- }
-
- @objc public func vapWrap_viewDidStopPlayMP4(_ frameIndex: Int, view: UIView) {
- if let vapView = self.vapView {
- self.viewDidStopPlayMP4(frameIndex, view: vapView)
- }
- }
-
- @objc public func vapWrap_onVAPStopWithError(_ error: NSError?, view: UIView) {
- // viewDidFailPlayMP4 方法只需要 error 参数
- self.viewDidFailPlayMP4(error ?? NSError(domain: "VAP", code: -1, userInfo: nil))
- }
-}
-
-// MARK: - HWDMP4PlayDelegate
-extension VapFlutterView: VAPWrapViewDelegate {
-
- func shouldStartPlayMP4(_ container: QGVAPWrapView, config: QGVAPConfigModel) -> Bool {
- var animConfigMap: [String: Any] = [:]
- animConfigMap["width"] = config.info.size.width
- animConfigMap["height"] = config.info.size.height
- animConfigMap["fps"] = config.info.fps
- animConfigMap["totalFrames"] = config.info.framesCount
- animConfigMap["videoHeight"] = config.info.videoSize.height
- animConfigMap["videoWidth"] = config.info.videoSize.width
- animConfigMap["isMix"] = config.info.isMerged
- animConfigMap["orien"] = config.info.targetOrientaion.rawValue
- animConfigMap["alphaPointRect"] = [
- "x": config.info.alphaAreaRect.minX,
- "y": config.info.alphaAreaRect.minY,
- "w": config.info.alphaAreaRect.maxX,
- "h": config.info.alphaAreaRect.maxY
- ]
- animConfigMap["rgbPointRect"] = [
- "x": config.info.rgbAreaRect.minX,
- "y": config.info.rgbAreaRect.minY,
- "w": config.info.rgbAreaRect.maxX,
- "h": config.info.rgbAreaRect.maxY
- ]
- animConfigMap["version"] = config.info.version
- sendVideoConfigReadyEvent(config: animConfigMap)
- return true
- }
-
- func viewDidStartPlayMP4(_ container: QGVAPWrapView) {
-
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onVideoStart", arguments: nil)
- }
- }
-
- func viewDidPlayMP4AtFrame(_ frame: QGMP4AnimatedImageFrame, view container: QGVAPWrapView) {
- let args: [String: Any] = ["frameIndex": frame.index]
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onVideoRender", arguments: args)
- }
- }
-
- func viewDidStopPlayMP4(_ lastFrameIndex: Int, view container: QGVAPWrapView) {
- playSuccess()
- DispatchQueue.main.async { [weak self] in
- self?.channel.invokeMethod("onVideoDestroy", arguments: nil)
- }
- }
-
- func viewDidFinishPlayMP4(_ totalFrameCount: Int, view container: QGVAPWrapView) {
- playSuccess()
- sendVideoCompleteEvent()
- }
-
- func viewDidFailPlayMP4(_ error: NSError) {
- // Handle specific VideoToolbox errors
- var errorMsg = error.localizedDescription
- var errorCode = error.code
-
- // Check for common VideoToolbox errors
- if error.domain == "com.apple.videotoolbox" || error.domain.contains("VT") {
- switch error.code {
- case -12909: // kVTVideoDecoderBadDataErr
- errorMsg = "Invalid or corrupted video data. Please check your VAP file encoding."
- errorCode = -1001
- case -12911: // kVTVideoDecoderMalfunctionErr
- errorMsg = "Video decoder malfunction. Try restarting the app."
- errorCode = -1002
- case -12912: // kVTVideoDecoderNotAvailableNowErr
- errorMsg = "Video decoder not available. Device may be under memory pressure."
- errorCode = -1003
- case -12913: // kVTInvalidSessionErr
- errorMsg = "Invalid video session. Please try playing the file again."
- errorCode = -1004
- default:
- errorMsg = "VideoToolbox error: \(error.localizedDescription)"
- errorCode = -1000
- }
- }
- playError(errorCode, "VIDEO_PLAYBACK_ERROR", errorMsg)
- sendFailedEvent(errorCode: errorCode, errorType:"VIDEO_PLAYBACK_ERROR", errorMsg: errorMsg)
- }
-
- func playError(_ code:Int, _ errorType:String, _ errorMsg:String){
- if(playResult != nil){
- playResult?(FlutterError(code: String(code),message: errorMsg,details: ["errorType": errorType]))
- playResult = nil
- }
- }
-
- func playSuccess(){
- if(playResult != nil){
- playResult?(nil)
- playResult = nil
- }
- }
- // MARK: - Resource Management Delegate Methods
- public func vapWrapview_content(forVapTag tag: String, resource info: QGVAPSourceInfo) -> String {
- guard let contentMap = vapTagContents[tag],
- let contentValue = contentMap["contentValue"] as? String else {
- return tag
- }
-
- // If the resource type is text, return the content value
- if info.type == .text || info.type == .textStr {
- return contentValue
- }
-
- // For other resource types (images, etc.), return the tag
- return tag
- }
-
- public func vapWrapView_loadVapImage(withURL urlStr: String, context: [AnyHashable : Any], completion completionBlock: @escaping VAPImageCompletionBlock) {
- print("URLNYA : " + urlStr + " INI VAPIMAGECONTENT : " + String(describing: vapTagContents) + "\n")
- // First check if we have content for this tag in vapTagContents
- if let contentMap = vapTagContents[urlStr],
- let contentValue = contentMap["contentValue"] as? String,
- let contentType = contentMap["contentType"] as? String {
- handleVapTagContent(content: contentValue, contentType: contentType, tag: urlStr, completion: completionBlock)
- return
- }
-
- // Fallback to original URL loading if no tag content is found
- guard let url = URL(string: urlStr) else {
- completionBlock(nil, NSError(domain: "VAPImageLoader", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL and no tag content found"]), urlStr)
- return
- }
-
- // Simple URLSession implementation - replace with your preferred image loading library
- URLSession.shared.dataTask(with: url) { data, response, error in
- DispatchQueue.main.async {
- if let error = error {
- completionBlock(nil, error as NSError, urlStr)
- return
- }
-
- guard let data = data, let image = UIImage(data: data) else {
- completionBlock(nil, NSError(domain: "VAPImageLoader", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to create image from data"]), urlStr)
- return
- }
-
- completionBlock(image, nil, urlStr)
- }
- }.resume()
- }
-
- func loadVapImageWithURL(_ urlStr: String, context: [String: Any], completion: @escaping VAPImageCompletionBlock) {
- // First check if we have content for this tag in vapTagContents
- if let contentMap = vapTagContents[urlStr],
- let contentValue = contentMap["contentValue"] as? String,
- let contentType = contentMap["contentType"] as? String {
- handleVapTagContent(content: contentValue, contentType: contentType, tag: urlStr, completion: completion)
- return
- }
-
- // Fallback to original URL loading if no tag content is found
- guard let url = URL(string: urlStr) else {
- completion(nil, NSError(domain: "VAPImageLoader", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL and no tag content found"]), urlStr)
- return
- }
-
- // Simple URLSession implementation - replace with your preferred image loading library
- URLSession.shared.dataTask(with: url) { data, response, error in
- DispatchQueue.main.async {
- if let error = error {
- completion(nil, error as NSError, urlStr)
- return
- }
-
- guard let data = data, let image = UIImage(data: data) else {
- completion(nil, NSError(domain: "VAPImageLoader", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to create image from data"]), urlStr)
- return
- }
-
- completion(image, nil, urlStr)
- }
- }.resume()
- }
-
- private func handleVapTagContent(content: String, contentType: String, tag: String, completion: @escaping VAPImageCompletionBlock) {
- NSLog("Processing tag: \(tag), contentType: \(contentType)")
-
- switch contentType {
- case "text":
- handleTextContent(content: content, tag: tag, completion: completion)
- case "image_base64":
- handleImageBase64Content(content: content, tag: tag, completion: completion)
- case "image_file":
- handleImageFileContent(content: content, tag: tag, completion: completion)
- case "image_asset":
- handleImageAssetContent(content: content, tag: tag, completion: completion)
- case "image_url":
- handleImageUrlContent(content: content, tag: tag, completion: completion)
- default:
- NSLog("Unsupported content type: \(contentType) for tag: \(tag)")
- completion(nil, NSError(domain: "VAPImageLoader", code: -4, userInfo: [NSLocalizedDescriptionKey: "Unsupported content type: \(contentType)"]), tag)
- }
- }
-
- // MARK: - Content Type Handlers
-
- private func handleTextContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) {
- // Text content is not handled by image loading, skip
- NSLog("Text content type for tag: \(tag), skipping image loading")
- completion(nil, nil, tag)
- }
-
- private func handleImageBase64Content(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) {
- DispatchQueue.global(qos: .userInitiated).async {
- var base64String = content
-
- // Remove data URL prefix if present (e.g., "data:image/png;base64,")
- if content.hasPrefix("data:image/") {
- if let commaRange = content.range(of: ",") {
- base64String = String(content[commaRange.upperBound...])
- }
- } else if content.hasPrefix("base64:") {
- base64String = String(content.dropFirst(7)) // Remove "base64:" prefix
- }
-
- guard let imageData = Data(base64Encoded: base64String),
- let image = UIImage(data: imageData) else {
- DispatchQueue.main.async {
- completion(nil, NSError(domain: "VAPImageLoader", code: -3, userInfo: [NSLocalizedDescriptionKey: "Failed to decode base64 image for tag: \(tag)"]), tag)
- }
- return
- }
-
- DispatchQueue.main.async {
- NSLog("Successfully decoded base64 image for tag: \(tag)")
- completion(image, nil, tag)
- }
- }
- }
-
- private func handleImageFileContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) {
- DispatchQueue.global(qos: .userInitiated).async {
- var fullPath = content
-
- // Handle different file path types
- if content.hasPrefix("/") {
- // Absolute path
- fullPath = content
- } else if content.hasPrefix("file://") {
- // File URL
- fullPath = String(content.dropFirst(7))
- } else {
- // Relative path - check in Documents directory
- let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
- let documentFilePath = documentsPath + "/" + content
-
- if FileManager.default.fileExists(atPath: documentFilePath) {
- fullPath = documentFilePath
- } else {
- // Use as-is and let it fail if invalid
- fullPath = content
- }
- }
-
- guard FileManager.default.fileExists(atPath: fullPath) else {
- DispatchQueue.main.async {
- completion(nil, NSError(domain: "VAPImageLoader", code: -4, userInfo: [NSLocalizedDescriptionKey: "File not found: \(fullPath) for tag: \(tag)"]), tag)
- }
- return
- }
-
- guard let image = UIImage(contentsOfFile: fullPath) else {
- DispatchQueue.main.async {
- completion(nil, NSError(domain: "VAPImageLoader", code: -5, userInfo: [NSLocalizedDescriptionKey: "Failed to load image from file: \(fullPath) for tag: \(tag)"]), tag)
- }
- return
- }
-
- DispatchQueue.main.async {
- NSLog("Successfully loaded file image for tag: \(tag) from: \(fullPath)")
- completion(image, nil, tag)
- }
- }
- }
-
- private func handleImageAssetContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) {
- DispatchQueue.global(qos: .userInitiated).async {
- // Try as Flutter asset first
- let assetKey = FlutterDartProject.lookupKey(forAsset: content)
- var fullPath: String?
-
- if let assetPath = Bundle.main.path(forResource: assetKey, ofType: nil) {
- fullPath = assetPath
- } else if let bundlePath = Bundle.main.path(forResource: content, ofType: nil) {
- // Try as direct bundle resource
- fullPath = bundlePath
- }
-
- guard let validPath = fullPath, FileManager.default.fileExists(atPath: validPath) else {
- DispatchQueue.main.async {
- completion(nil, NSError(domain: "VAPImageLoader", code: -4, userInfo: [NSLocalizedDescriptionKey: "Asset not found: \(content) for tag: \(tag)"]), tag)
- }
- return
- }
-
- guard let image = UIImage(contentsOfFile: validPath) else {
- DispatchQueue.main.async {
- completion(nil, NSError(domain: "VAPImageLoader", code: -5, userInfo: [NSLocalizedDescriptionKey: "Failed to load asset image: \(content) for tag: \(tag)"]), tag)
- }
- return
- }
-
- DispatchQueue.main.async {
- NSLog("Successfully loaded asset image for tag: \(tag) from: \(validPath)")
- completion(image, nil, tag)
- }
- }
- }
-
- private func handleImageUrlContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) {
- guard let url = URL(string: content) else {
- completion(nil, NSError(domain: "VAPImageLoader", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(content) for tag: \(tag)"]), tag)
- return
- }
-
- NSLog("Loading image from URL: \(content) for tag: \(tag)")
-
- URLSession.shared.dataTask(with: url) { data, response, error in
- DispatchQueue.main.async {
- if let error = error {
- NSLog("Failed to load image from URL: \(content) for tag: \(tag), error: \(error.localizedDescription)")
- completion(nil, error as NSError, tag)
- return
- }
-
- guard let data = data, let image = UIImage(data: data) else {
- completion(nil, NSError(domain: "VAPImageLoader", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to create image from URL data: \(content) for tag: \(tag)"]), tag)
- return
- }
-
- NSLog("Successfully loaded image from URL: \(content) for tag: \(tag)")
- completion(image, nil, tag)
- }
- }.resume()
- }
-
-}
+import Flutter
+import UIKit
+
+public class VapFlutterView: NSObject, FlutterPlatformView {
+ private let containerView: UIView
+ private let channel: FlutterMethodChannel
+ private var vapTagContents: [String: [String: Any]] = [:]
+ private var isMuted = false
+ private var repeatCount = 0
+ private var scaleType = "fitCenter"
+
+ init(
+ context: CGRect,
+ params: [String: Any]?,
+ messenger: FlutterBinaryMessenger,
+ id: Int64
+ ) {
+ containerView = UIView(frame: context)
+ channel = FlutterMethodChannel(name: "vap_view_\(id)", binaryMessenger: messenger)
+ super.init()
+
+ containerView.backgroundColor = .clear
+ channel.setMethodCallHandler(onMethodCall)
+
+ if let params {
+ if let initialScaleType = params["scaleType"] as? String {
+ scaleType = initialScaleType
+ }
+ if let initialLoop = params["loop"] as? Int {
+ repeatCount = initialLoop
+ }
+ if let initialMute = params["mute"] as? Bool {
+ isMuted = initialMute
+ }
+ if let tagContents = params["vapTagContents"] as? [String: [String: Any]] {
+ vapTagContents = tagContents
+ }
+ }
+ }
+
+ public func view() -> UIView {
+ containerView
+ }
+
+ private func onMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ switch call.method {
+ case "dispose":
+ channel.setMethodCallHandler(nil)
+ result(nil)
+ case "playFile", "playAsset":
+ notifyUnsupportedPlayback(call.method)
+ result(nil)
+ case "stop":
+ result(nil)
+ case "setLoop":
+ if let args = call.arguments as? [String: Any], let loop = args["loop"] as? Int {
+ repeatCount = loop
+ }
+ result(nil)
+ case "setMute":
+ if let args = call.arguments as? [String: Any], let mute = args["mute"] as? Bool {
+ isMuted = mute
+ }
+ result(nil)
+ case "setScaleType":
+ if let args = call.arguments as? [String: Any], let value = args["scaleType"] as? String {
+ scaleType = value
+ }
+ result(nil)
+ case "setVapTagContent":
+ if let args = call.arguments as? [String: Any],
+ let tag = args["tag"] as? String,
+ let content = args["content"] as? [String: Any] {
+ vapTagContents[tag] = content
+ }
+ result(nil)
+ case "setVapTagContents":
+ if let args = call.arguments as? [String: Any],
+ let contents = args["contents"] as? [String: [String: Any]] {
+ vapTagContents = contents
+ }
+ result(nil)
+ case "getVapTagContent":
+ if let args = call.arguments as? [String: Any], let tag = args["tag"] as? String {
+ result(vapTagContents[tag])
+ } else {
+ result(nil)
+ }
+ case "getAllVapTagContents":
+ result(vapTagContents)
+ case "clearVapTagContents":
+ vapTagContents.removeAll()
+ result(nil)
+ default:
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
+ private func notifyUnsupportedPlayback(_ method: String) {
+ channel.invokeMethod("onFailed", arguments: [
+ "errorCode": "-1",
+ "type": "unsupported",
+ "msg": "QGVAPlayer is disabled for this iOS build. \(method) is unavailable."
+ ])
+ }
+}
diff --git a/local_packages/tancent_vap-1.0.0+1/ios/tancent_vap.podspec b/local_packages/tancent_vap-1.0.0+1/ios/tancent_vap.podspec
index da46c6e..2be575f 100644
--- a/local_packages/tancent_vap-1.0.0+1/ios/tancent_vap.podspec
+++ b/local_packages/tancent_vap-1.0.0+1/ios/tancent_vap.podspec
@@ -14,17 +14,12 @@ VAP is a high-performance animation framework developed by Tencent for playing t
s.license = { :file => '../LICENSE' }
s.author = { 'Laskarmedia' => 'support@laskarmedia.id' }
s.source = { :path => '.' }
- s.source_files = 'Classes/**/*'
- s.dependency 'Flutter'
- s.platform = :ios, '12.0'
- s.dependency 'QGVAPlayer', '= 1.0.19'
-
-
- # VAP iOS SDK dependency (users need to add the git source in their Podfile)
- # s.dependency 'QGVAPlayer'
-
- # Flutter.framework does not contain a i386 slice.
- s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+ s.source_files = 'Classes/**/*'
+ s.dependency 'Flutter'
+ s.platform = :ios, '12.0'
+
+ # Flutter.framework does not contain a i386 slice.
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
diff --git a/pubspec.yaml b/pubspec.yaml
index 648bd3a..b627eb3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -85,11 +85,11 @@ dependencies:
#相册
# wechat_assets_picker: ^9.5.0
# wechat_camera_picker: ^4.2.0
- image_picker: ^1.2.0
- #图片裁剪
- image_cropper: ^5.0.0
- loading_indicator_view_plus: ^2.0.0
- back_button_interceptor: ^8.0.4
+ image_picker: ^1.2.0
+ #图片裁剪
+ image_cropper:
+ path: ./local_packages/image_cropper-5.0.1-patched
+ back_button_interceptor: ^8.0.4
#vap特效
# flutter_vap_plus: ^1.2.10
# tancent_vap: ^1.0.0+1
@@ -125,18 +125,11 @@ dependencies:
marquee: ^2.3.0
extended_nested_scroll_view: ^6.2.1
event_bus: ^2.0.1
- readmore: ^3.0.0
- #分享
- social_sharing_plus: ^1.2.3
- #外链跳转
- app_links: ^6.4.1
- flutter_foreground_task:
- path: ./local_packages/flutter_foreground_task-9.1.0
- #本地音乐检索
- on_audio_query:
- path: ./local_packages/on_audio_query-2.9.0
-
-dev_dependencies:
+ readmore: ^3.0.0
+ #外链跳转
+ app_links: ^6.4.1
+
+dev_dependencies:
flutter_launcher_icons: ^0.14.4
flutter_test:
sdk: flutter
@@ -172,4 +165,4 @@ flutter:
- sc_images/msg/
- sc_images/level/
- sc_images/coupon/
- - fonts/
\ No newline at end of file
+ - fonts/
diff --git a/需求进度.md b/需求进度.md
new file mode 100644
index 0000000..789dc4a
--- /dev/null
+++ b/需求进度.md
@@ -0,0 +1,99 @@
+# 需求进度
+
+## 当前总目标
+- 分析当前 Flutter 包体过大的主要原因,识别多余组件、冗余依赖、过大资源或不合理构建配置,并给出可执行的优化建议。
+
+## 已完成模块
+- 创建进度跟踪文件。
+- 初步确认仓库结构与当前工作区状态。
+- 完成第一轮静态体积排查,拿到目录体积与大文件分布。
+- 完成第二轮依赖使用情况排查,识别出未直接引用的重型插件与高风险依赖。
+- 完成第一轮高置信度瘦身改动,移除 4 个当前未在业务层使用的插件依赖并清理相关平台声明。
+- 完成构建阻塞项定位,确认 `image_cropper 5.0.1` 是当前 Android 构建失败的主要兼容性来源,并已切换到本地修补版依赖。
+- 完成 Android 签名配置兜底,解决当前仓库缺少 `yumi.jks` / `yumi_debug.jks` 时无法继续本地构建分析的问题。
+- 完成 Android release 包体构建与体积分析,拿到当前 APK 的真实大小和主要组成。
+
+## 进行中模块
+- 汇总最终结论,整理造成包体冗余的主因与后续优化优先级。
+
+## 关键技术决策
+- 先做无侵入体检:优先分析依赖、资源体积和构建配置,暂不直接删除业务代码或资源。
+- 避开仓库中已有未提交改动,只在必要文件上新增或追加分析结果。
+- 重点关注发布包相关项,而不是被 `build/`、iOS 中间产物这类本地构建缓存误导。
+- 发布包体分析优先使用单 ABI 做 `--analyze-size`,因为当前工程配置了多 ABI,Flutter 不允许直接对多 ABI 产物做代码体积分析。
+- 实际清理仅选择高置信度冗余项:`loading_indicator_view_plus`、`social_sharing_plus`、`flutter_foreground_task`、`on_audio_query`,暂不动 Agora、腾讯 IM、支付、WebView 等核心能力依赖。
+- 对 `image_cropper` 不做升级式重构,先采用本地 path 依赖加最小兼容补丁的方式恢复构建,避免改动业务裁剪调用代码。
+- 对 Android 签名配置采用条件回退策略:存在正式 keystore 时走正式签名,不存在时回退到 debug 签名,仅用于本地分析和构建验证。
+- 即便传入 `--target-platform android-arm64`,当前 Gradle 的 `abiFilters` 仍会把 `arm64-v8a`、`armeabi-v7a`、`x86_64` 三套 so 一起打进 APK,因此后续瘦身必须回到 Gradle ABI 配置本身处理。
+
+## 已改动文件
+- `需求进度.md`
+- `pubspec.yaml`
+- `lib/ui_kit/components/dialog/dialog.dart`
+- `android/app/src/main/AndroidManifest.xml`
+- `ios/Runner/Info.plist`
+- `local_packages/image_cropper-5.0.1-patched/android/src/main/java/vn/hunghd/flutter/plugins/imagecropper/ImageCropperPlugin.java`
+- `local_packages/image_cropper-5.0.1-patched/`
+- `android/app/build.gradle.kts`
+
+## 已验证结果
+- 仓库存在 `pubspec.yaml`、Android/iOS 构建配置以及大量图片资源和多个 `local_packages` 本地包。
+- 当前工作区存在用户已有未提交改动,后续分析将避免覆盖。
+- 目录体积显示:`build/` 约 4.7G、`.dart_tool/` 约 347M、`ios/` 约 250M、`sc_images/` 约 48M、`local_packages/` 约 6.1M。
+- `android/app/build.gradle.kts` 中 release 构建关闭了 `minify`、`shrinkResources`、`zipAlign`,会直接放大发布包体。
+- Android ABI 当前包含 `arm64-v8a`、`armeabi-v7a`、`x86_64`,如果打通用包会把不必要架构一起带进去。
+- 当前资源中存在多张 2MB 以上的大图,例如 `sc_images/person/sc_icon_edit_userinfo_bg.png`、`sc_images/splash/sc_splash.png` 和多张 2.2MB 左右的 `webp` 背景图。
+- 资源子目录中较大的部分主要是:`sc_images/index/` 约 18M、`sc_images/room/` 约 17M、`sc_images/person/` 约 5M、`sc_images/splash/` 约 2.5M。
+- 本地包体积中较大的部分主要是:`local_packages/tancent_vap-1.0.0+1` 约 4.2M、`local_packages/flutter_foreground_task-9.1.0` 约 1.1M。
+- 依赖引用扫描结果显示:`social_sharing_plus`、`flutter_foreground_task`、`on_audio_query` 当前在 `lib/` 与 `test/` 中没有直接 import,属于优先核查是否可移除的候选项。
+- 明确在业务中使用的重型能力包括:`agora_rtc_engine`、`tencent_cloud_chat_sdk`、`tancent_vap`、`flutter_svga`、`firebase_auth/core/crashlytics`、`in_app_purchase`、`webview_flutter`。
+- 执行 `flutter build apk --release --analyze-size` 时失败,Flutter 明确提示当前多 ABI 配置下必须指定单一 `--target-platform` 才能输出体积分析。
+- `app-debug.apk` 当前约 405MB;拆包后可见最大组成是三套 ABI 的 `libflutter.so`、Agora 系列 so、腾讯 IM so,以及 `sc_images/index`、`sc_images/room` 等大资源目录。
+- `.flutter-plugins-dependencies` 已证明 Android 端存在大量 `native_build` 插件注册;即使 Dart 代码没有 import,插件仍会进入原生构建链路。
+- 已将 `loading_indicator_view_plus` 的使用替换为 Flutter 自带 `CircularProgressIndicator`,为后续移除该插件铺平路径。
+- 已从工程依赖中删除 `loading_indicator_view_plus`、`social_sharing_plus`、`flutter_foreground_task`、`on_audio_query`,并同步清理 Android 前台服务与媒体音频权限、iOS 前台任务标识与媒体库用途说明。
+- `flutter pub get` 后,上述 4 个冗余插件已不再出现在 Flutter 当前插件注册链路中;搜索结果仅残留在未重新执行 `pod install` 的 `ios/Podfile.lock` 旧记录里。
+- 继续构建时又发现 `image_cropper 5.0.1` 的 Android 插件源码仍引用已移除的 `PluginRegistry.Registrar`,这会同时阻塞 debug/release。
+- 已复制 `image_cropper 5.0.1` 到工作区本地包,并删除其过时的 `registerWith` 兼容入口,准备再次验证构建。
+- 当前仓库下 `android/app/yumi.jks` 与 `android/app/yumi_debug.jks` 都不存在,原始 Gradle 配置会在签名校验阶段阻塞 release 构建。
+- 已将 Gradle 配置调整为:正式 keystore 缺失时自动回退到 debug 签名,从而继续完成本地分析构建。
+- `flutter build apk --release --target-platform android-arm64 --analyze-size` 已成功执行,生成 `build/app/outputs/flutter-apk/app-release.apk`。
+- 当前 release APK 文件大小约 `271MB`,构建输出显示约 `284.6MB`,分析总压缩体积约 `271MB`。
+- 当前 release APK 的压缩后主要组成是:
+- `lib/arm64-v8a` 约 `101.3MB`
+- `lib/x86_64` 约 `64.4MB`
+- `lib/armeabi-v7a` 约 `58.2MB`
+- `assets/flutter_assets` 约 `49.9MB`
+- `classes.dex + classes2.dex + classes3.dex` 合计约 `8.8MB`
+- 三套 ABI 合计约 `223.8MB`,其中 `x86_64 + armeabi-v7a` 两套额外架构合计约 `122.5MB`,是当前包体过大的头号原因。
+- release APK 中最大的 native so 主要来自 Agora 与腾讯 IM:
+- `libagora-rtc-sdk.so` 合计约 `79.4MB`
+- `libagora-ffmpeg.so` 合计约 `17.8MB`
+- `libagora_lip_sync_extension.so` 合计约 `13.7MB`
+- `libagora_spatial_audio_extension.so` 合计约 `13.4MB`
+- `libImSDK.so` 合计约 `10.2MB`
+- `libflutter.so` 约 `11.3MB`
+- release APK 中最大的资源目录主要是:
+- `sc_images/index` 约 `18.9MB`
+- `sc_images/room` 约 `16.9MB`
+- `sc_images/person` 约 `5.1MB`
+- `sc_images/level` 约 `2.9MB`
+- `sc_images/splash` 约 `2.6MB`
+- 单文件大图中比较明显的包括:
+- `sc_images/person/sc_icon_edit_userinfo_bg.png` 约 `2.64MB`
+- `sc_images/splash/sc_splash.png` 约 `2.37MB`
+- 多张 `sc_icon_gamebroad_lv*.webp` 单张约 `2.0MB - 2.35MB`
+- Flutter 的 Dart AOT 符号在分析里约 `11MB`,其中 `package:yumi` 自身约 `2MB`,说明业务 Dart 代码不是主要膨胀来源。
+- `MaterialIcons-Regular.otf` 在 release 中已自动 tree-shake 到约 `3.7KB`,图标字体不是问题。
+- 本轮为恢复构建额外处理了两个工程兼容问题:`loading_indicator_view_plus` 缺少 Android namespace、`image_cropper 5.0.1` 旧注册接口不兼容当前 Flutter/AGP。
+
+## 已知问题
+- `ios/Podfile.lock` 还保留旧插件记录,因为本轮未执行 `pod install`;但 Flutter 当前依赖与插件注册链路已经不再包含已移除插件。
+- 目前 release 仍关闭了 `minify` / `shrinkResources` / `zipAlign`,这部分尚未动手优化,因为可能影响 Agora、腾讯 IM、Firebase 等反射或资源引用行为。
+- Android 构建配置中的 `abiFilters` 仍然固定包含三套架构,导致单 ABI 分析构建时 APK 里依旧带入 `x86_64` 与 `armeabi-v7a`。
+
+## 下一步要做什么
+- 调整 Android ABI 策略,至少移除 `x86_64`,并根据发包方式决定是否保留 `armeabi-v7a`。
+- 压缩或替换 `sc_images/index`、`sc_images/room`、`sc_images/person` 中的超大图片资源。
+- 评估 Agora 扩展能力是否都需要,优先核查 lip sync、spatial audio、clear vision、segmentation、face capture 等扩展库能否裁剪。
+- 在补齐必要 keep rules 后,再尝试开启 `minify`、`shrinkResources`、`zipAlign` 做第二轮正式瘦身。