This commit is contained in:
NIGGER SLAYER 2026-04-13 12:05:59 +08:00
parent 60d5c427de
commit 36780a0789
37 changed files with 3194 additions and 1491 deletions

View File

@ -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"))

View File

@ -9,26 +9,12 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- required -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<!-- for utils -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- 谷歌支付权限 -->
<uses-permission android:name="com.android.vending.BILLING" />
<!-- 这个权限会触发谷歌审核拦截,暂时用不上,移除 -->
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"
tools:node="remove" />
<queries>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 谷歌支付权限 -->
<uses-permission android:name="com.android.vending.BILLING" />
<queries>
<!-- Facebook -->
<package android:name="com.facebook.katana" />
<!-- WhatsApp -->
@ -118,17 +104,11 @@
android:name="com.google.android.gms.auth.api.signin.GoogleSignInActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel" />
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:exported="false"
android:foregroundServiceType="microphone"
android:stopWithTask="true" />
</application>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

View File

@ -1,498 +1,439 @@
PODS:
- agora_rtc_engine (6.5.3):
- AgoraIrisRTC_iOS (= 4.5.2-build.1)
- AgoraRtcEngine_iOS (= 4.5.2)
- Flutter
- AgoraInfra_iOS (1.2.13.1)
- AgoraIrisRTC_iOS (4.5.2-build.1)
- AgoraRtcEngine_iOS (4.5.2):
- AgoraRtcEngine_iOS/AIAEC (= 4.5.2)
- AgoraRtcEngine_iOS/AIAECLL (= 4.5.2)
- AgoraRtcEngine_iOS/AINS (= 4.5.2)
- AgoraRtcEngine_iOS/AINSLL (= 4.5.2)
- AgoraRtcEngine_iOS/AudioBeauty (= 4.5.2)
- AgoraRtcEngine_iOS/ClearVision (= 4.5.2)
- AgoraRtcEngine_iOS/ContentInspect (= 4.5.2)
- AgoraRtcEngine_iOS/FaceCapture (= 4.5.2)
- AgoraRtcEngine_iOS/FaceDetection (= 4.5.2)
- AgoraRtcEngine_iOS/LipSync (= 4.5.2)
- AgoraRtcEngine_iOS/ReplayKit (= 4.5.2)
- AgoraRtcEngine_iOS/RtcBasic (= 4.5.2)
- AgoraRtcEngine_iOS/SpatialAudio (= 4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecDec (= 4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecEnc (= 4.5.2)
- AgoraRtcEngine_iOS/VideoCodecDec (= 4.5.2)
- AgoraRtcEngine_iOS/VideoCodecEnc (= 4.5.2)
- AgoraRtcEngine_iOS/VirtualBackground (= 4.5.2)
- AgoraRtcEngine_iOS/VQA (= 4.5.2)
- AgoraRtcEngine_iOS/AIAEC (4.5.2)
- AgoraRtcEngine_iOS/AIAECLL (4.5.2)
- AgoraRtcEngine_iOS/AINS (4.5.2)
- AgoraRtcEngine_iOS/AINSLL (4.5.2)
- AgoraRtcEngine_iOS/AudioBeauty (4.5.2)
- AgoraRtcEngine_iOS/ClearVision (4.5.2)
- AgoraRtcEngine_iOS/ContentInspect (4.5.2)
- AgoraRtcEngine_iOS/FaceCapture (4.5.2)
- AgoraRtcEngine_iOS/FaceDetection (4.5.2)
- AgoraRtcEngine_iOS/LipSync (4.5.2)
- AgoraRtcEngine_iOS/ReplayKit (4.5.2)
- AgoraRtcEngine_iOS/RtcBasic (4.5.2):
- AgoraInfra_iOS (= 1.2.13.1)
- AgoraRtcEngine_iOS/SpatialAudio (4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecDec (4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecEnc (4.5.2)
- AgoraRtcEngine_iOS/VideoCodecDec (4.5.2)
- AgoraRtcEngine_iOS/VideoCodecEnc (4.5.2)
- AgoraRtcEngine_iOS/VirtualBackground (4.5.2)
- AgoraRtcEngine_iOS/VQA (4.5.2)
- app_links (6.4.1):
- Flutter
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- Firebase/Auth (11.15.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.15.0)
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Crashlytics (11.15.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 11.15.0)
- firebase_auth (5.7.0):
- Firebase/Auth (= 11.15.0)
- firebase_core
- Flutter
- firebase_core (3.15.2):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_crashlytics (4.3.10):
- Firebase/Crashlytics (= 11.15.0)
- firebase_core
- Flutter
- FirebaseAppCheckInterop (11.15.0)
- FirebaseAuth (11.15.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.15.0)
- FirebaseCoreExtension (~> 11.15.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (11.15.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0)
- FirebaseSessions (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseRemoteConfigInterop (11.15.0)
- FirebaseSessions (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseCoreExtension (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- Flutter (1.0.0)
- flutter_foreground_task (0.0.1):
- Flutter
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- fluttertoast (0.0.2):
- Flutter
- google_sign_in_ios (0.0.1):
- AppAuth (>= 1.7.4)
- Flutter
- FlutterMacOS
- GoogleSignIn (~> 8.0)
- GTMSessionFetcher (>= 3.4.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleSignIn (8.0.0):
- AppAuth (< 2.0, >= 1.7.3)
- AppCheckCore (~> 11.0)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
- AppAuth/Core (~> 1.7)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher (3.5.0):
- GTMSessionFetcher/Full (= 3.5.0)
- GTMSessionFetcher/Core (3.5.0)
- GTMSessionFetcher/Full (3.5.0):
- GTMSessionFetcher/Core
- HydraAsync (2.0.6)
- image_cropper (0.0.4):
- Flutter
- TOCropViewController (~> 2.6.1)
- image_picker_ios (0.0.1):
- Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- iris_method_channel (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- loading_indicator_view_plus (0.0.1):
- Flutter
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- on_audio_query_ios (0.0.1):
- Flutter
- SwiftyBeaver
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- QGVAPlayer (1.0.19)
- RecaptchaInterop (101.0.0)
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- social_sharing_plus (0.0.1):
- Flutter
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- store_redirect (0.0.1):
- Flutter
- SwiftyBeaver (1.9.5)
- tancent_vap (0.0.1):
- Flutter
- QGVAPlayer (= 1.0.19)
- tencent_cloud_chat_sdk (8.0.0):
- Flutter
- HydraAsync
- TXIMSDK_Plus_iOS_XCFramework (= 8.3.6498)
- TOCropViewController (2.6.1)
- TXIMSDK_Plus_iOS_XCFramework (8.3.6498)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- video_thumbnail (0.0.1):
- Flutter
- libwebp
- wakelock_plus (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- agora_rtc_engine (from `.symlinks/plugins/agora_rtc_engine/ios`)
- app_links (from `.symlinks/plugins/app_links/ios`)
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- Flutter (from `Flutter`)
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- iris_method_channel (from `.symlinks/plugins/iris_method_channel/ios`)
- loading_indicator_view_plus (from `.symlinks/plugins/loading_indicator_view_plus/ios`)
- on_audio_query_ios (from `.symlinks/plugins/on_audio_query_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- social_sharing_plus (from `.symlinks/plugins/social_sharing_plus/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- store_redirect (from `.symlinks/plugins/store_redirect/ios`)
- tancent_vap (from `.symlinks/plugins/tancent_vap/ios`)
- tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- AgoraInfra_iOS
- AgoraIrisRTC_iOS
- AgoraRtcEngine_iOS
- AppAuth
- AppCheckCore
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseRemoteConfigInterop
- FirebaseSessions
- GoogleDataTransport
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- HydraAsync
- libwebp
- Mantle
- nanopb
- OrderedSet
- PromisesObjC
- PromisesSwift
- QGVAPlayer
- RecaptchaInterop
- SDWebImage
- SDWebImageWebPCoder
- SwiftyBeaver
- TOCropViewController
- TXIMSDK_Plus_iOS_XCFramework
EXTERNAL SOURCES:
agora_rtc_engine:
:path: ".symlinks/plugins/agora_rtc_engine/ios"
app_links:
:path: ".symlinks/plugins/app_links/ios"
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
Flutter:
:path: Flutter
flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios"
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
google_sign_in_ios:
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
image_cropper:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
iris_method_channel:
:path: ".symlinks/plugins/iris_method_channel/ios"
loading_indicator_view_plus:
:path: ".symlinks/plugins/loading_indicator_view_plus/ios"
on_audio_query_ios:
:path: ".symlinks/plugins/on_audio_query_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
social_sharing_plus:
:path: ".symlinks/plugins/social_sharing_plus/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
store_redirect:
:path: ".symlinks/plugins/store_redirect/ios"
tancent_vap:
:path: ".symlinks/plugins/tancent_vap/ios"
tencent_cloud_chat_sdk:
:path: ".symlinks/plugins/tencent_cloud_chat_sdk/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
video_thumbnail:
:path: ".symlinks/plugins/video_thumbnail/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
agora_rtc_engine: 0c7d50312967c4dc31c3c45e50589ce48f57e08a
AgoraInfra_iOS: 3691b2b277a1712a35ae96de25af319de0d73d08
AgoraIrisRTC_iOS: eab58c126439adf5ec99632828a558ea216860da
AgoraRtcEngine_iOS: 97e2398a2addda9057815a2a583a658e36796ff6
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
firebase_crashlytics: 30dcd6dfd2fe895c0848af46722a4227346c19aa
FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df
FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b
FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseCrashlytics: e09d0bc19aa54a51e45b8039c836ef73f32c039a
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa
FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063
image_cropper: 655b3ba703c9e15e3111e79151624d6154288774
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
loading_indicator_view_plus: 33a5d3527e9c6a7b470c79878613d01c4fa1fb65
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
QGVAPlayer: a0bca68c9bd6f1c8de5ac2d10ddf98be6038cce9
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
social_sharing_plus: e6024862e5a4be59ef8c97a93558cba1043628bf
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
store_redirect: 55fd455802ceab09803b2df6e68f19a58815266a
SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82
tancent_vap: 4917210cc7e916023fd112655170d8e3d074f482
tencent_cloud_chat_sdk: 55e5fffe20f6b7937a26a674ccccb639563a9790
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
TXIMSDK_Plus_iOS_XCFramework: 5d1933192fb3b7ef2fe933f1623de4a0486a7fe2
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
PODFILE CHECKSUM: a6f49a93e5f85201a2efdadcd7cf184b0e310894
COCOAPODS: 1.16.2
PODS:
- agora_rtc_engine (6.5.3):
- AgoraIrisRTC_iOS (= 4.5.2-build.1)
- AgoraRtcEngine_iOS (= 4.5.2)
- Flutter
- AgoraInfra_iOS (1.2.13.1)
- AgoraIrisRTC_iOS (4.5.2-build.1)
- AgoraRtcEngine_iOS (4.5.2):
- AgoraRtcEngine_iOS/AIAEC (= 4.5.2)
- AgoraRtcEngine_iOS/AIAECLL (= 4.5.2)
- AgoraRtcEngine_iOS/AINS (= 4.5.2)
- AgoraRtcEngine_iOS/AINSLL (= 4.5.2)
- AgoraRtcEngine_iOS/AudioBeauty (= 4.5.2)
- AgoraRtcEngine_iOS/ClearVision (= 4.5.2)
- AgoraRtcEngine_iOS/ContentInspect (= 4.5.2)
- AgoraRtcEngine_iOS/FaceCapture (= 4.5.2)
- AgoraRtcEngine_iOS/FaceDetection (= 4.5.2)
- AgoraRtcEngine_iOS/LipSync (= 4.5.2)
- AgoraRtcEngine_iOS/ReplayKit (= 4.5.2)
- AgoraRtcEngine_iOS/RtcBasic (= 4.5.2)
- AgoraRtcEngine_iOS/SpatialAudio (= 4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecDec (= 4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecEnc (= 4.5.2)
- AgoraRtcEngine_iOS/VideoCodecDec (= 4.5.2)
- AgoraRtcEngine_iOS/VideoCodecEnc (= 4.5.2)
- AgoraRtcEngine_iOS/VirtualBackground (= 4.5.2)
- AgoraRtcEngine_iOS/VQA (= 4.5.2)
- AgoraRtcEngine_iOS/AIAEC (4.5.2)
- AgoraRtcEngine_iOS/AIAECLL (4.5.2)
- AgoraRtcEngine_iOS/AINS (4.5.2)
- AgoraRtcEngine_iOS/AINSLL (4.5.2)
- AgoraRtcEngine_iOS/AudioBeauty (4.5.2)
- AgoraRtcEngine_iOS/ClearVision (4.5.2)
- AgoraRtcEngine_iOS/ContentInspect (4.5.2)
- AgoraRtcEngine_iOS/FaceCapture (4.5.2)
- AgoraRtcEngine_iOS/FaceDetection (4.5.2)
- AgoraRtcEngine_iOS/LipSync (4.5.2)
- AgoraRtcEngine_iOS/ReplayKit (4.5.2)
- AgoraRtcEngine_iOS/RtcBasic (4.5.2):
- AgoraInfra_iOS (= 1.2.13.1)
- AgoraRtcEngine_iOS/SpatialAudio (4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecDec (4.5.2)
- AgoraRtcEngine_iOS/VideoAv1CodecEnc (4.5.2)
- AgoraRtcEngine_iOS/VideoCodecDec (4.5.2)
- AgoraRtcEngine_iOS/VideoCodecEnc (4.5.2)
- AgoraRtcEngine_iOS/VirtualBackground (4.5.2)
- AgoraRtcEngine_iOS/VQA (4.5.2)
- app_links (6.4.1):
- Flutter
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- Firebase/Auth (11.15.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.15.0)
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Crashlytics (11.15.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 11.15.0)
- firebase_auth (5.7.0):
- Firebase/Auth (= 11.15.0)
- firebase_core
- Flutter
- firebase_core (3.15.2):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_crashlytics (4.3.10):
- Firebase/Crashlytics (= 11.15.0)
- firebase_core
- Flutter
- FirebaseAppCheckInterop (11.15.0)
- FirebaseAuth (11.15.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.15.0)
- FirebaseCoreExtension (~> 11.15.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (11.15.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0)
- FirebaseSessions (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseRemoteConfigInterop (11.15.0)
- FirebaseSessions (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseCoreExtension (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- Flutter (1.0.0)
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- fluttertoast (0.0.2):
- Flutter
- google_sign_in_ios (0.0.1):
- AppAuth (>= 1.7.4)
- Flutter
- FlutterMacOS
- GoogleSignIn (~> 8.0)
- GTMSessionFetcher (>= 3.4.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleSignIn (8.0.0):
- AppAuth (< 2.0, >= 1.7.3)
- AppCheckCore (~> 11.0)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
- AppAuth/Core (~> 1.7)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher (3.5.0):
- GTMSessionFetcher/Full (= 3.5.0)
- GTMSessionFetcher/Core (3.5.0)
- GTMSessionFetcher/Full (3.5.0):
- GTMSessionFetcher/Core
- HydraAsync (2.0.6)
- image_cropper (0.0.4):
- Flutter
- TOCropViewController (~> 2.6.1)
- image_picker_ios (0.0.1):
- Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- iris_method_channel (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- RecaptchaInterop (101.0.0)
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- tancent_vap (0.0.1):
- Flutter
- tencent_cloud_chat_sdk (8.0.0):
- Flutter
- HydraAsync
- TXIMSDK_Plus_iOS_XCFramework (= 8.3.6498)
- TOCropViewController (2.6.1)
- TXIMSDK_Plus_iOS_XCFramework (8.3.6498)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- video_thumbnail (0.0.1):
- Flutter
- libwebp
- wakelock_plus (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- agora_rtc_engine (from `.symlinks/plugins/agora_rtc_engine/ios`)
- app_links (from `.symlinks/plugins/app_links/ios`)
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- Flutter (from `Flutter`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- iris_method_channel (from `.symlinks/plugins/iris_method_channel/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- tancent_vap (from `.symlinks/plugins/tancent_vap/ios`)
- tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- AgoraInfra_iOS
- AgoraIrisRTC_iOS
- AgoraRtcEngine_iOS
- AppAuth
- AppCheckCore
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseRemoteConfigInterop
- FirebaseSessions
- GoogleDataTransport
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- HydraAsync
- libwebp
- Mantle
- nanopb
- PromisesObjC
- PromisesSwift
- RecaptchaInterop
- SDWebImage
- SDWebImageWebPCoder
- TOCropViewController
- TXIMSDK_Plus_iOS_XCFramework
EXTERNAL SOURCES:
agora_rtc_engine:
:path: ".symlinks/plugins/agora_rtc_engine/ios"
app_links:
:path: ".symlinks/plugins/app_links/ios"
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
Flutter:
:path: Flutter
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
google_sign_in_ios:
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
image_cropper:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
iris_method_channel:
:path: ".symlinks/plugins/iris_method_channel/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
tancent_vap:
:path: ".symlinks/plugins/tancent_vap/ios"
tencent_cloud_chat_sdk:
:path: ".symlinks/plugins/tencent_cloud_chat_sdk/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
video_thumbnail:
:path: ".symlinks/plugins/video_thumbnail/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
agora_rtc_engine: 0c7d50312967c4dc31c3c45e50589ce48f57e08a
AgoraInfra_iOS: 3691b2b277a1712a35ae96de25af319de0d73d08
AgoraIrisRTC_iOS: eab58c126439adf5ec99632828a558ea216860da
AgoraRtcEngine_iOS: 97e2398a2addda9057815a2a583a658e36796ff6
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
firebase_crashlytics: 30dcd6dfd2fe895c0848af46722a4227346c19aa
FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df
FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b
FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseCrashlytics: e09d0bc19aa54a51e45b8039c836ef73f32c039a
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa
FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063
image_cropper: 655b3ba703c9e15e3111e79151624d6154288774
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
tancent_vap: fa8ad93814a9f950514a7074c662d2c937084c68
tencent_cloud_chat_sdk: 55e5fffe20f6b7937a26a674ccccb639563a9790
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
TXIMSDK_Plus_iOS_XCFramework: 5d1933192fb3b7ef2fe933f1623de4a0486a7fe2
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
PODFILE CHECKSUM: a6f49a93e5f85201a2efdadcd7cf184b0e310894
COCOAPODS: 1.16.2

View File

@ -46,10 +46,8 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppleMusicUsageDescription</key>
<string>We need access to the media library so that you can play your favorite local music files in the voice background.</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera so you can take a photo and make it a profile picture, or take and share a picture in real time in a voice chat room.</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera so you can take a photo and make it a profile picture, or take and share a picture in real time in a voice chat room.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone so that you can make live calls, record, and send voice messages with other users in the voice dating room.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
@ -75,13 +73,9 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.pravera.flutter_foreground_task.refresh</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>com.apple.developer.applesignin</key>
<array>

View File

@ -286,93 +286,135 @@ class _MePage2State extends State<MePage2> {
);
}
Widget _buildMenuCard2() {
final userProfile = context.watch<SocialChatUserProfileManager>();
final items = <_MenuRowData>[];
// 1.
if (userProfile.userIdentity?.anchor ?? false) {
if (userProfile.userIdentity?.agent ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.agentCenter,
assetIcon: 'sc_images/index/at_icon_agent_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false",
);
},
),
);
} else {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.hostCenter,
assetIcon: 'sc_images/index/at_icon_host_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false",
);
},
),
);
}
}
// 2. BD
if (!(userProfile.userIdentity?.admin ?? false)) {
if (userProfile.userIdentity?.bdLeader ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.bdLeader,
assetIcon: 'sc_images/index/at_icon_bd_leader.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false",
);
},
),
);
} else if (userProfile.userIdentity?.bd ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.bdCenter,
assetIcon: 'sc_images/index/at_icon_bd_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdCenterUrl)}&showTitle=false",
);
},
),
);
}
}
// 3.
if (userProfile.userIdentity?.freightAgent ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.rechargeAgency,
assetIcon: 'sc_images/index/at_icon_recharge_agency.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false",
);
},
),
);
}
// 4. BD逻辑有重叠
if (userProfile.userIdentity?.admin ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.adminCenter,
Widget _buildMenuCard2() {
final items = <_MenuRowData>[];
/*
// 1.
if (userProfile.userIdentity?.anchor ?? false) {
if (userProfile.userIdentity?.agent ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.agentCenter,
assetIcon: 'sc_images/index/at_icon_agent_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false",
);
},
),
);
} else {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.hostCenter,
assetIcon: 'sc_images/index/at_icon_host_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false",
);
},
),
);
}
}
// 2. BD
if (!(userProfile.userIdentity?.admin ?? false)) {
if (userProfile.userIdentity?.bdLeader ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.bdLeader,
assetIcon: 'sc_images/index/at_icon_bd_leader.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false",
);
},
),
);
} else if (userProfile.userIdentity?.bd ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.bdCenter,
assetIcon: 'sc_images/index/at_icon_bd_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdCenterUrl)}&showTitle=false",
);
},
),
);
}
}
// 3.
if (userProfile.userIdentity?.freightAgent ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.rechargeAgency,
assetIcon: 'sc_images/index/at_icon_recharge_agency.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false",
);
},
),
);
}
*/
// Debug: 便 WebView
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.hostCenter,
assetIcon: 'sc_images/index/sc_icon_host_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false",
);
},
),
);
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.bdCenter,
assetIcon: 'sc_images/index/sc_icon_bd_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdCenterUrl)}&showTitle=false",
);
},
),
);
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.rechargeAgency,
assetIcon: 'sc_images/index/sc_icon_recharge_agency.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false",
);
},
),
);
// 4. BD逻辑有重叠
final userProfile = context.watch<SocialChatUserProfileManager>();
if (userProfile.userIdentity?.admin ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.adminCenter,
assetIcon: 'sc_images/index/at_icon_admin_center.png',
onTap: () {
SCNavigatorUtils.push(

View File

@ -1,13 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:loading_indicator_view_plus/loading_indicator_view_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
typedef OnFutureFunc = Future<dynamic> Function();
typedef OnSuccess = Function(dynamic v);
class STLoadingDialog extends Dialog {
typedef OnFutureFunc = Future<dynamic> Function();
typedef OnSuccess = Function(dynamic v);
class _DialogLoadingIndicator extends StatelessWidget {
const _DialogLoadingIndicator();
@override
Widget build(BuildContext context) {
return SizedBox(
width: 32.w,
height: 32.w,
child: const CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xffFF50BC)),
),
);
}
}
class STLoadingDialog extends Dialog {
String text;
Widget? centerContent;
final GestureTapCallback? verifyCallback;
@ -43,16 +58,14 @@ class STLoadingDialog extends Dialog {
child: AspectRatio(
aspectRatio: 1,
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
BallClipRotateMultipleIndicator(
color: Color(0xffFF50BC),
),
Text(
"${text ?? "loading..."}",
style: TextStyle(fontSize: 14.sp, color: Color(0xff888888)),
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const _DialogLoadingIndicator(),
Text(
"${text ?? "loading..."}",
style: TextStyle(fontSize: 14.sp, color: Color(0xff888888)),
)
],
),
@ -124,16 +137,14 @@ class _FutureLoadingDialogState extends State<FutureLoadingDialog> {
child: AspectRatio(
aspectRatio: 1,
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
BallClipRotateMultipleIndicator(
color: Color(0xffFF50BC),
),
Text(
"${widget.text ?? "loading..."}",
style: TextStyle(fontSize: 14.sp, color: Color(0xff888888)),
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const _DialogLoadingIndicator(),
Text(
"${widget.text ?? "loading..."}",
style: TextStyle(fontSize: 14.sp, color: Color(0xff888888)),
)
],
),

View File

@ -0,0 +1,283 @@
## 5.0.1
* Android: improve compression quality
## 5.0.0
* upgrade `http` to v1.0.0
## 4.0.1
* refactor CHANGELOG
## 4.0.0
* Android: support namespace, Gradle 8
## 3.0.3
* iOS: bump ios minimum version to 11
## 3.0.2
* Web: fix web translation issue
## 3.0.1
* Android: replace `jcenter` by `mavenCentral`
* Web: support localization
## 3.0.0
* **BREAKING CHANGE**: move all setting models to platform interface packages.
* Separate Javascript codes from `WebSettings` model (so there's no need to use conditional import to config Web UI now)
### ***MIGRATION GUIDE***
#### **BEFORE**
```dart
WebUiSettings(
boundary: Boundary(
width: 520,
height: 520,
),
viewPort: ViewPort(width: 480, height: 480, type: 'circle'),
)
```
#### **AFTER**
```dart
WebUiSettings(
boundary: const CroppieBoundary(
width: 520,
height: 520,
),
viewPort: const CroppieViewPort(width: 480, height: 480, type: 'circle'),
)
```
## 2.0.3
* correct importing JS library
## 2.0.2
* update document
## 2.0.1
* fix missing pubspec config on web
## 2.0.0
* Support Web
* **BREAKING CHANGE**:
- change result data type from `File` to `CroppedFile`.
- remove `androidUiSettings` and `iosUiSettings` parameter in `cropImage` method, they are replaced by `uiSettings`
### ***MIGRATION GUIDE***
#### **BEFORE**
```dart
File croppedFile = await ImageCropper().cropImage(
sourcePath: imageFile.path,
androidUiSettings: AndroidUiSettings(
toolbarTitle: 'Cropper',
toolbarColor: Colors.deepOrange,
toolbarWidgetColor: Colors.white,
initAspectRatio: CropAspectRatioPreset.original,
lockAspectRatio: false),
iosUiSettings: IOSUiSettings(
minimumAspectRatio: 1.0,
)
);
```
#### **AFTER**
```dart
CroppedFile croppedFile = await ImageCropper().cropImage(
sourcePath: imageFile.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'Cropper',
toolbarColor: Colors.deepOrange,
toolbarWidgetColor: Colors.white,
initAspectRatio: CropAspectRatioPreset.original,
lockAspectRatio: false),
IOSUiSettings(
title: 'Cropper',
)
],
);
```
## 2.0.0-beta.3
* support rotate image on Web
## 2.0.0-beta.2
* correct missing README document about Web support.
## 2.0.0-beta.1
* **BREAKING CHANGE**: update result models to support web, replace `File` by `CroppedFile`
## 2.0.0-beta
* migrate to federated plugins
* **BREAKING CHANGE**: remove `androidUiSettings` and `iosUiSettings`, they are replaced by `uiSettings` for sake of supporting multiple platforms in future.
### ***MIGRATION GUIDE***
#### **BEFORE**
```dart
File croppedFile = await ImageCropper().cropImage(
sourcePath: imageFile.path,
androidUiSettings: AndroidUiSettings(
toolbarTitle: 'Cropper',
toolbarColor: Colors.deepOrange,
toolbarWidgetColor: Colors.white,
initAspectRatio: CropAspectRatioPreset.original,
lockAspectRatio: false),
iosUiSettings: IOSUiSettings(
minimumAspectRatio: 1.0,
)
);
```
#### **AFTER**
```dart
File croppedFile = await ImageCropper().cropImage(
sourcePath: imageFile.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'Cropper',
toolbarColor: Colors.deepOrange,
toolbarWidgetColor: Colors.white,
initAspectRatio: CropAspectRatioPreset.original,
lockAspectRatio: false),
IOSUiSettings(
title: 'Cropper',
)
],
);
```
## 1.5.1
* update README to introduce Web support experiment
## 1.5.0
* Upgrade `uCrop` to v2.2.8
* Upgrade `TOCropViewController` to v2.6.1
* PR: [#309](https://github.com/hnvn/flutter_image_cropper/pull/309), #[229](https://github.com/hnvn/flutter_image_cropper/pull/299), #[294](https://github.com/hnvn/flutter_image_cropper/pull/294), [#258](https://github.com/hnvn/flutter_image_cropper/pull/258), [#251](https://github.com/hnvn/flutter_image_cropper/pull/258)
* **BREAKING CHANGE**: change `cropImage()` to instance method
## 1.4.1
* Upgrade `uCrop` to v2.2.7
## 1.4.0
* Upgrade `TOCropViewController` to v2.6.0
* Migrate to nullsafety
## 1.3.1
* Upgrade `uCrop` to v2.2.6
* Fix bug: image compress file format (png)
## 1.3.0
* Update `pubspec` to new format
* Upgrade `TOCropViewController` to v2.5.4
## 1.2.3
* Android: fix bug
## 1.2.2
* Android: upgrade uCrop to v2.2.5
* **BREAKING CHANGE**: remove `activeWidgetColor` from `AndroidUiSettings`
## 1.2.1
* iOS: add more UI customization properties (title, initRect)
* update Flutter version constraint (>= v1.12.13)
## 1.2.0
* Android: migrate to embedding v2
## 1.1.2
* Android: fix bug crashing on Flutter v1.12.13
## 1.1.1
* Android: upgrade gradle version
* iOS: remove `static_framework` Pod configuration, upgrade `TOCropViewController` to 2.5.2
## 1.1.0
* **BIG UPDATES**: supports UI customization on both of Android and iOS, supports more image compress format and quality.
* **BREAKING CHANGE**: `ratioX` and `ratioY` are replaced by `aspectRatio`, `circleShape` is replaced by `cropStyle`, removed `toolbarTitle` and `toolbarColor` (these properties are moved into `AndroidUiSettings`)
* iOS: upgrade native library (TOCropViewController v2.5.1)
* Android: upgrade native library (uCrop v2.2.4)
## 1.0.2
* iOS: upgrade native library
* Android: fix bug #40
## 1.0.1
* Android: migrate to AndroidX
* upgrade native libraries
## 1.0.0
* Android: remove deprecated support libraries
* Android: target version to 28
## 0.0.9
* clarify code document
## 0.0.8
* set default value for `circleShape`
## 0.0.7
* support circular cropping
## 0.0.6
* upgrade `TOCropViewController` dependency to v2.3.8
## 0.0.5
* re-config to support Dart2
* fix bug: lock aspect ratio
## 0.0.4
* refactor: change `toolbarColor` type of `int` to `Color`
## 0.0.3
* fix bug: increasing image size after cropping
* add new feature: `toolbarTitle` and `toolbarColor` to customize title and color of copper `Activity`
## 0.0.2
* clarify document
## 0.0.1
* initial release.

View File

@ -0,0 +1,26 @@
Copyright 2013, the Dart project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,275 @@
# Image Cropper
[![pub package](https://img.shields.io/pub/v/image_cropper.svg)](https://pub.dartlang.org/packages/image_cropper)
A Flutter plugin for Android, iOS and Web supports cropping images. This plugin is based on three different native libraries so it comes with different UI between these platforms.
## Introduction
**Image Cropper** doesn't manipulate images in Dart codes directly, instead, the plugin uses [Platform Channel](https://flutter.dev/docs/development/platform-integration/platform-channels) to expose Dart APIs that Flutter application can use to communicate with three very powerful native libraries ([uCrop](https://github.com/Yalantis/uCrop), [TOCropViewController](https://github.com/TimOliver/TOCropViewController) and [croppie](https://github.com/Foliotek/Croppie)) to crop and rotate images. Because of that, all credits belong to these libraries.
### uCrop - Yalantis
[![GitHub watchers](https://img.shields.io/github/watchers/Yalantis/uCrop.svg?style=social&label=Watch&maxAge=2592000)](https://GitHub.com/Yalantis/uCrop/watchers/) [![GitHub stars](https://img.shields.io/github/stars/Yalantis/uCrop.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/Yalantis/uCrop/stargazers/) [![GitHub forks](https://img.shields.io/github/forks/Yalantis/uCrop.svg?style=social&label=Fork&maxAge=2592000)](https://GitHub.com/Yalantis/uCrop/network/) [![](https://jitpack.io/v/Yalantis/uCrop.svg)](https://jitpack.io/#Yalantis/uCrop) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
This project aims to provide an ultimate and flexible image cropping experience. Made in [Yalantis](https://yalantis.com/?utm_source=github)
<p align="center">
<img src="https://github.com/hnvn/flutter_image_cropper/blob/v2/image_cropper/screenshots/android_demo.gif?raw=true" width="200" />
</p>
### TOCropViewController - TimOliver
[![GitHub watchers](https://img.shields.io/github/watchers/TimOliver/TOCropViewController.svg?style=social&label=Watch&maxAge=2592000)](https://GitHub.com/TimOliver/TOCropViewController/watchers/) [![GitHub stars](https://img.shields.io/github/stars/TimOliver/TOCropViewController.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/TimOliver/TOCropViewController/stargazers/) [![GitHub forks](https://img.shields.io/github/forks/TimOliver/TOCropViewController.svg?style=social&label=Fork&maxAge=2592000)](https://GitHub.com/TimOliver/TOCropViewController/network/) [![Version](https://img.shields.io/cocoapods/v/TOCropViewController.svg?style=flat)](http://cocoadocs.org/docsets/TOCropViewController) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/TimOliver/TOCropViewController/master/LICENSE)
`TOCropViewController` is an open-source `UIViewController` subclass to crop out sections of `UIImage` objects, as well as perform basic rotations. It is excellent for things like editing profile pictures, or sharing parts of a photo online. It has been designed with the iOS Photos app editor in mind, and as such, behaves in a way that should already feel familiar to users of iOS.
<p align="center">
<img src="https://github.com/hnvn/flutter_image_cropper/blob/v2/image_cropper/screenshots/ios_demo.gif?raw=true" width="200" />
</p>
### Croppie - Foliotek
[![GitHub watchers](https://img.shields.io/github/watchers/foliotek/croppie.svg?style=social&label=Watch&maxAge=2592000)](https://GitHub.com/foliotek/croppie/watchers/) [![GitHub stars](https://img.shields.io/github/stars/foliotek/croppie.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/foliotek/croppie/stargazers/) [![GitHub forks](https://img.shields.io/github/forks/foliotek/croppie.svg?style=social&label=Fork&maxAge=2592000)](https://GitHub.com/foliotek/croppie/network/) [![npm version](https://badge.fury.io/js/croppie.svg)](https://badge.fury.io/js/croppie)
Croppie is a fast, easy to use image cropping plugin with tons of configuration options!
<p align="center">
<img src="https://github.com/hnvn/flutter_image_cropper/blob/v2/image_cropper/screenshots/croppie_preview.png?raw=true" width="400"/>
</p>
## How to install
### Android
- Add UCropActivity into your AndroidManifest.xml
````xml
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
````
#### Note:
From v1.2.0, you need to migrate your android project to v2 embedding ([detail](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects))
### iOS
- No configuration required
### Web
- Add following codes inside `<head>` tag in file `web/index.html`:
```html
<head>
....
<!-- Croppie -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.css" />
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/exif-js/2.3.0/exif.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.js"></script>
....
</head>
```
## 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
<details>
<summary>Click to view detail</summary>
<br/>
**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 |
</details>
### iOS
<details>
<summary>Click to view detail</summary>
<br/>
**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 |
</details>
### Web
<details>
<summary>Click to view detail</summary>
<br/>
**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'),
)
]
);
},
),
);
},
...
)
````
</details>
## 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)

View File

@ -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'
}
}

View File

@ -0,0 +1,3 @@
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536M

View File

@ -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

View File

@ -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" "$@"

View File

@ -0,0 +1 @@
rootProject.name = 'image_cropper'

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="vn.hunghd.flutter.plugins.imagecropper">
</manifest>

View File

@ -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());
}
}

View File

@ -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<String> 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<AspectRatio> 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);
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface FLTImageCropperPlugin : NSObject<FlutterPlugin>
@end

View File

@ -0,0 +1,316 @@
#import "FLTImageCropperPlugin.h"
#import <TOCropViewController/TOCropViewController.h>
#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <Photos/Photos.h>
@interface FLTImageCropperPlugin() <TOCropViewControllerDelegate>
@end
@implementation FLTImageCropperPlugin {
FlutterResult _result;
NSDictionary *_arguments;
UIViewController *_viewController;
float _compressQuality;
NSString *_compressFormat;
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)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

View File

@ -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

View File

@ -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';

View File

@ -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<CroppedFile?> cropImage({
required String sourcePath,
int? maxWidth,
int? maxHeight,
CropAspectRatio? aspectRatio,
List<CropAspectRatioPreset> aspectRatioPresets = const [
CropAspectRatioPreset.original,
CropAspectRatioPreset.square,
CropAspectRatioPreset.ratio3x2,
CropAspectRatioPreset.ratio4x3,
CropAspectRatioPreset.ratio16x9
],
CropStyle cropStyle = CropStyle.rectangle,
ImageCompressFormat compressFormat = ImageCompressFormat.jpg,
int compressQuality = 90,
List<PlatformUiSettings>? 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<CroppedFile?> recoverImage() {
return platform.recoverImage();
}
}

View File

@ -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"

View File

@ -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

View File

@ -1,2 +1,2 @@
sdk.dir=D:\\AndroidSDK
flutter.sdk=D:\\FlutterSDK\\flutter_windows_3.29.3-stable\\flutter
sdk.dir=/Users/nigger/Library/Android/sdk
flutter.sdk=/Users/nigger/Desktop/develop/flutter

View File

@ -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 --")

View File

@ -0,0 +1,5 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

View File

@ -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

View File

@ -0,0 +1,19 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GeneratedPluginRegistrant_h
#define GeneratedPluginRegistrant_h
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface GeneratedPluginRegistrant : NSObject
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
@end
NS_ASSUME_NONNULL_END
#endif /* GeneratedPluginRegistrant_h */

View File

@ -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"

View File

@ -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"

View File

@ -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."
])
}
}

View File

@ -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

View File

@ -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/
- fonts/

99
需求进度.md Normal file
View File

@ -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`,因为当前工程配置了多 ABIFlutter 不允许直接对多 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` 做第二轮正式瘦身。