├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── crazylegend │ │ └── mediapicker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── crazylegend │ │ │ └── mediapicker │ │ │ ├── FragmentResult.kt │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_checked.xml │ │ ├── ic_close.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_unchecked.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── crazylegend │ └── mediapicker │ └── ExampleUnitTest.kt ├── audiopicker ├── .gitignore ├── README.md ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── screens │ ├── screen_1.png │ └── screen_3.png └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── crazylegend │ │ └── audiopicker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── crazylegend │ │ │ └── audiopicker │ │ │ ├── adapters │ │ │ ├── AudioDiffUtil.kt │ │ │ ├── multi │ │ │ │ ├── AudioMultiSelectAdapter.kt │ │ │ │ └── AudioMultiSelectViewHolder.kt │ │ │ └── single │ │ │ │ ├── AudioSingleAdapter.kt │ │ │ │ └── AudioSingleViewHolder.kt │ │ │ ├── audios │ │ │ ├── AudioModel.kt │ │ │ └── AudiosVM.kt │ │ │ ├── consts │ │ │ └── DialogConsts.kt │ │ │ ├── contracts │ │ │ ├── MultiPickerContracts.kt │ │ │ └── SinglePickerContracts.kt │ │ │ ├── dialogs │ │ │ ├── multi │ │ │ │ └── MultiAudioPickerBottomSheetDialog.kt │ │ │ └── single │ │ │ │ └── SingleAudioPickerBottomSheetDialog.kt │ │ │ ├── listeners │ │ │ ├── onAudioPicked.kt │ │ │ ├── onAudiosPicked.kt │ │ │ └── onShouldRecycleBitmaps.kt │ │ │ ├── modifiers │ │ │ ├── MultiAudioPickerModifier.kt │ │ │ └── SingleAudioPickerModifier.kt │ │ │ └── pickers │ │ │ ├── MultiAudioPicker.kt │ │ │ └── SingleAudioPicker.kt │ └── res │ │ ├── drawable │ │ ├── ic_album.xml │ │ └── ic_album_second.xml │ │ └── layout │ │ └── itemview_audio.xml │ └── test │ └── java │ └── com │ └── crazylegend │ └── audiopicker │ └── ExampleUnitTest.kt ├── build.gradle ├── core ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── crazylegend │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── crazylegend │ │ │ └── core │ │ │ ├── Extensions.kt │ │ │ ├── abstracts │ │ │ ├── AbstractAVM.kt │ │ │ └── AbstractBottomSheetDialogFragment.kt │ │ │ ├── adapters │ │ │ ├── BaseViewHolder.kt │ │ │ ├── multi │ │ │ │ ├── MultiSelectAdapter.kt │ │ │ │ └── MultiSelectViewHolder.kt │ │ │ └── single │ │ │ │ ├── SingleAdapter.kt │ │ │ │ ├── SingleDiffUtil.kt │ │ │ │ └── SingleViewHolder.kt │ │ │ ├── consts │ │ │ └── Consts.kt │ │ │ ├── contracts │ │ │ ├── BaseContractMultiPick.kt │ │ │ └── BaseContractSinglePick.kt │ │ │ ├── dto │ │ │ ├── BaseCursorModel.kt │ │ │ └── PickerConfig.kt │ │ │ ├── modifiers │ │ │ ├── SizeTextModifier.kt │ │ │ ├── TitleTextModifier.kt │ │ │ ├── base │ │ │ │ ├── BaseMultiPickerModifier.kt │ │ │ │ └── BaseSinglePickerModifier.kt │ │ │ ├── multi │ │ │ │ ├── DoneButtonModifier.kt │ │ │ │ └── SelectIconModifier.kt │ │ │ └── single │ │ │ │ └── ImageModifier.kt │ │ │ └── sorting │ │ │ └── SortOrder.kt │ └── res │ │ ├── anim │ │ ├── fragment_fade_enter.xml │ │ └── fragment_fade_exit.xml │ │ ├── drawable │ │ ├── ic_checked_default.xml │ │ ├── ic_close.xml │ │ ├── ic_image.xml │ │ ├── ic_minus.xml │ │ ├── ic_unchecked_default.xml │ │ └── rounded_bg_abstract_dialog.xml │ │ ├── layout │ │ ├── fragment_images_gallery_layout.xml │ │ ├── fragment_images_gallery_layout_multi.xml │ │ └── itemview_image.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── crazylegend │ └── core │ └── ExampleUnitTest.kt ├── extensions ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── crazylegend │ │ └── extensions │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── crazylegend │ │ └── extensions │ │ ├── Extensions.kt │ │ ├── FragmentViewBindingDelegate.kt │ │ └── ViewBindingExtensions.kt │ └── test │ └── java │ └── com │ └── crazylegend │ └── extensions │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── imagepicker ├── .gitignore ├── README.md ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── screens │ ├── screen_1.png │ └── screen_3.png └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── crazylegend │ │ └── imagepicker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── crazylegend │ │ └── imagepicker │ │ ├── consts │ │ └── DialogConsts.kt │ │ ├── contracts │ │ ├── MultiPickerContracts.kt │ │ └── SinglePickerContracts.kt │ │ ├── dialogs │ │ ├── multi │ │ │ └── MultiImagePickerBottomSheetDialog.kt │ │ └── single │ │ │ └── SingleImagePickerBottomSheetDialog.kt │ │ ├── images │ │ ├── ImageModel.kt │ │ └── ImagesVM.kt │ │ ├── listeners │ │ ├── onImagePicked.kt │ │ └── onImagesPicked.kt │ │ └── pickers │ │ ├── MultiImagePicker.kt │ │ └── SingleImagePicker.kt │ └── test │ └── java │ └── com │ └── crazylegend │ └── imagepicker │ └── ExampleUnitTest.kt ├── jitpack.yml ├── settings.gradle └── videopicker ├── .gitignore ├── README.md ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro ├── screens ├── screen_1.png └── screen_3.png └── src ├── androidTest └── java │ └── com │ └── crazylegend │ └── videopicker │ └── ExampleInstrumentedTest.kt ├── main ├── AndroidManifest.xml └── java │ └── com │ └── crazylegend │ └── videopicker │ ├── consts │ └── DialogConsts.kt │ ├── contracts │ ├── MultiPickerContracts.kt │ └── SinglePickerContracts.kt │ ├── dialogs │ ├── multi │ │ └── MultiVideoPickerBottomSheetDialog.kt │ └── single │ │ └── SingleVideoPickerBottomSheetDialog.kt │ ├── listeners │ ├── onVideoPicked.kt │ └── onVideosPicked.kt │ ├── pickers │ ├── MultiVideoPicker.kt │ └── SingleVideoPicker.kt │ └── videos │ ├── VideoModel.kt │ └── VideosVM.kt └── test └── java └── com └── crazylegend └── videopicker └── ExampleUnitTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # MediaPicker 6 | ### Kotlin Android library to pick images, videos and audios 7 | 8 | ![](https://jitpack.io/v/FunkyMuse/MediaPicker.svg) 9 | 10 | ## Usage 11 | 1. Add JitPack to your project build.gradle 12 | 13 | ```gradle 14 | allprojects { 15 | repositories { 16 | ... 17 | maven { url 'https://jitpack.io' } 18 | } 19 | } 20 | ``` 21 | 22 | #### or 23 | 24 | If your Android studio version is Arctic Fox and above then add it in your settings.gradle: 25 | 26 | ```gradle 27 | dependencyResolutionManagement { 28 | repositories { 29 | ... 30 | maven { url 'https://jitpack.io' } 31 | } 32 | } 33 | ``` 34 | 35 | 2. Add the dependency in the application build.gradle 36 | 37 | ```gradle 38 | dependencies { 39 | def pickerVersion = "1.0.0" //look-up the latest one on jitpack 40 | 41 | //images 42 | implementation "com.github.FunkyMuse.MediaPicker:imagepicker:$pickerVersion" 43 | //audios 44 | implementation "com.github.FunkyMuse.MediaPicker:audiopicker:$pickerVersion" 45 | //videos 46 | implementation "com.github.FunkyMuse.MediaPicker:videopicker:$pickerVersion" 47 | } 48 | ``` 49 | 50 | 3. To not run into any issues in your application build.gradle add 51 | 52 | ```gradle 53 | compileOptions { 54 | sourceCompatibility = 1.8 55 | targetCompatibility = 1.8 56 | } 57 | 58 | kotlinOptions { 59 | jvmTarget = "1.8" 60 | } 61 | 62 | viewBinding { 63 | enabled = true 64 | } 65 | ``` 66 | 67 | 4. How to use and screens 68 | 69 | [Image picker](https://github.com/FunkyMuse/MediaPicker/tree/master/imagepicker) 70 | 71 | [Video picker](https://github.com/FunkyMuse/MediaPicker/tree/master/videopicker) 72 | 73 | [Audio picker](https://github.com/FunkyMuse/MediaPicker/tree/master/audiopicker) 74 | 75 | [Sample app](https://github.com/FunkyMuse/MediaPicker/blob/master/app/src/main/java/com/crazylegend/mediapicker/MainActivity.kt) 76 | 77 | ## Contributing 78 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 79 | 80 | ## License 81 | [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) 82 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | 4 | testImplementation 'junit:junit:4.13.2' 5 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 6 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 7 | 8 | //glide 9 | implementation "com.github.bumptech.glide:glide:$glide" 10 | kapt "com.github.bumptech.glide:compiler:$glide" 11 | 12 | //core 13 | implementation "androidx.activity:activity-ktx:$activity" 14 | implementation "androidx.fragment:fragment-ktx:$fragment" 15 | implementation "androidx.appcompat:appcompat:$appCompat" 16 | implementation "androidx.core:core-ktx:$coreKTX" 17 | 18 | //ui 19 | implementation "androidx.constraintlayout:constraintlayout:$constraint_layout" 20 | implementation "androidx.recyclerview:recyclerview:$recycler" 21 | implementation "com.google.android.material:material:$material" 22 | 23 | //live data 24 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle" 25 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle" 26 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle" 27 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle" 28 | } 29 | 30 | android { 31 | namespace 'com.crazylegend.mediapicker' 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/crazylegend/mediapicker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.mediapicker 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.crazylegend.mediapicker", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 13 | 14 | 15 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_checked.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_unchecked.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MediaPicker 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 13 | 14 | 17 | 18 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/test/java/com/crazylegend/mediapicker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.mediapicker 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /audiopicker/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /audiopicker/README.md: -------------------------------------------------------------------------------- 1 | Current jitpack version: [![](https://jitpack.io/v/FunkyMuse/MediaPicker.svg)](https://jitpack.io/#FunkyMuse/MediaPicker) 2 | 3 | 1. You must declare the permission in your manifest 4 | ```xml 5 | 6 | ``` 7 | 2. If you haven't included the dependencies from the previous screen now's a good time 8 | ```gradle 9 | dependencies { 10 | def pickerVersion = "1.0.0" //look-up the latest one on jitpack 11 | 12 | //the core package is a must 13 | implementation "com.github.FunkyMuse.MediaPicker:core:$pickerVersion" 14 | 15 | //audios 16 | implementation "com.github.FunkyMuse.MediaPicker:audiopicker:$pickerVersion" 17 | } 18 | ``` 19 | 3. How to use single picker and check out [how to customize single audio picker](https://github.com/FunkyMuse/MediaPicker/wiki/Single-audio-picker-customization) 20 | ```kotlin 21 | //simple usage without customization 22 | SingleAudioPicker.showPicker(this) { 23 | loadAudio(it) 24 | } 25 | 26 | 27 | //customized 28 | SingleAudioPicker.showPicker(this, { 29 | setupViewHolderTitleText { 30 | textColor = Color.BLACK 31 | textPadding = 10 // use dp or sp this is only for demonstration purposes 32 | } 33 | setupBaseModifier( 34 | loadingIndicatorColor = R.color.minusColor, 35 | titleTextModifications = { 36 | textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START 37 | textStyle = TitleTextModifier.TextStyle.ITALIC 38 | textColor = Color.BLACK 39 | marginBottom = 30 // use dp or sp this is only for demonstration purposes 40 | textPadding = 5 // use dp or sp this is only for demonstration purposes 41 | textSize = 30f // use sp this is only for demonstration purposes 42 | textString = "Pick an audio" 43 | }, 44 | placeHolderModifications = { 45 | resID = R.drawable.ic_image 46 | } 47 | ) 48 | }, ::loadAudio) 49 | 50 | ``` 51 | 52 | 4. How to use multi picker and check out [how to customize multi audio picker](https://github.com/FunkyMuse/MediaPicker/wiki/Multi-audio-picker-customization) 53 | ```kotlin 54 | //simple usage without customization 55 | MultiAudioPicker.showPicker(this) { 56 | loadAudios(it) 57 | } 58 | 59 | 60 | //customized 61 | MultiAudioPicker.showPicker(this, { 62 | setupViewHolderTitleText { 63 | textColor = ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryDark) 64 | textStyle = TitleTextModifier.TextStyle.BOLD 65 | textPadding = 10 // use dp or sp this is only for demonstration purposes 66 | } 67 | setupBaseMultiPicker( 68 | tintForLoadingProgressBar = ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryDark), 69 | gravityForSelectAndUnSelectIndicators = BaseMultiPickerModifier.Gravity.BOTTOM_LEFT, 70 | titleModifications = { 71 | textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START 72 | textStyle = TitleTextModifier.TextStyle.ITALIC 73 | textColor = Color.BLACK 74 | marginBottom = 30 // use dp or sp this is only for demonstration purposes 75 | textPadding = 5 // use dp or sp this is only for demonstration purposes 76 | textSize = 30f // use sp this is only for demonstration purposes 77 | textString = "Pick multiple songs" 78 | }, 79 | selectIconModifications = { 80 | resID = R.drawable.ic_checked 81 | tint = Color.BLACK 82 | }, 83 | unSelectIconModifications = { 84 | resID = R.drawable.ic_unchecked 85 | tint = Color.BLACK 86 | }, 87 | viewHolderPlaceholderModifications = { 88 | resID = R.drawable.ic_album_second 89 | } 90 | ) 91 | }, ::doSomethingWithAudioList) 92 | ``` 93 | 94 | If you're using Fragments to call the pickers you can leverage [set fragment result listener](https://developer.android.com/reference/androidx/fragment/app/FragmentManager#setfragmentresultlistener) to get back the result and you don't have to restore the listener nor provide a lambda for the listener, it can be as simple as 95 | ```kotlin 96 | SingleAudioPicker.showPicker(requireContext()) 97 | ``` 98 | ```kotlin 99 | 100 | setFragmentResultListener(SingleAudioPicker.SINGLE_AUDIO_REQUEST_KEY) { _, bundle -> 101 | val loadedModel = bundle.getParcelable(SingleAudioPicker.ON_SINGLE_AUDIO_PICK_KEY) 102 | loadedModel?.let { loadAudio(it) } 103 | } 104 | 105 | setFragmentResultListener(MultiAudioPicker.MULTI_AUDIO_REQUEST_KEY) { _, bundle -> 106 | val loadedModel = bundle.getParcelableArrayList(MultiAudioPicker.ON_MULTI_AUDIO_PICK_KEY) 107 | loadedModel?.let { doSomethingWithAudioList(it) } 108 | } 109 | ``` 110 | ## 111 | If you're still not sure how to use, take a look at the [Sample app](https://github.com/FunkyMuse/MediaPicker/blob/master/app/src/main/java/com/crazylegend/mediapicker/MainActivity.kt) 112 | 113 | ## 114 | If you're still not sure how to use fragment listener, take a look at the [Sample app](https://github.com/FunkyMuse/MediaPicker/blob/master/app/src/main/java/com/crazylegend/mediapicker/FragmentResult.kt#L330) 115 | 116 | ## Screens 117 | 118 | Single picker 119 | 120 | 121 | 122 | Multi picker 123 | 124 | 125 | -------------------------------------------------------------------------------- /audiopicker/build.gradle: -------------------------------------------------------------------------------- 1 | android { 2 | namespace 'com.crazylegend.audiopicker' 3 | } -------------------------------------------------------------------------------- /audiopicker/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/audiopicker/consumer-rules.pro -------------------------------------------------------------------------------- /audiopicker/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /audiopicker/screens/screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/audiopicker/screens/screen_1.png -------------------------------------------------------------------------------- /audiopicker/screens/screen_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/audiopicker/screens/screen_3.png -------------------------------------------------------------------------------- /audiopicker/src/androidTest/java/com/crazylegend/audiopicker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.crazylegend.audiopicker.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /audiopicker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/adapters/AudioDiffUtil.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.adapters 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.crazylegend.audiopicker.audios.AudioModel 5 | 6 | 7 | /** 8 | * Created by crazy on 5/11/20 to long live and prosper ! 9 | */ 10 | internal class AudioDiffUtil : DiffUtil.ItemCallback() { 11 | override fun areItemsTheSame(oldItem: AudioModel, newItem: AudioModel) = 12 | oldItem.id == newItem.id 13 | 14 | override fun areContentsTheSame(oldItem: AudioModel, newItem: AudioModel) = 15 | (oldItem.id == newItem.id) && (oldItem.isSelected == newItem.isSelected) 16 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/adapters/multi/AudioMultiSelectAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.adapters.multi 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.ListAdapter 5 | import com.crazylegend.audiopicker.adapters.AudioDiffUtil 6 | import com.crazylegend.audiopicker.audios.AudioModel 7 | import com.crazylegend.audiopicker.databinding.ItemviewAudioBinding 8 | import com.crazylegend.core.inflater 9 | import com.crazylegend.core.modifiers.TitleTextModifier 10 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 11 | import com.crazylegend.core.modifiers.single.ImageModifier 12 | 13 | 14 | /** 15 | * Created by crazy on 5/8/20 to long live and prosper ! 16 | */ 17 | internal class AudioMultiSelectAdapter(private val modifier: BaseMultiPickerModifier?, 18 | private val viewHolderPlaceholderModifier: ImageModifier?, 19 | private val viewHolderTitleTextModifier: TitleTextModifier?) : 20 | ListAdapter(AudioDiffUtil()) { 21 | 22 | 23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudioMultiSelectViewHolder { 24 | val holder = AudioMultiSelectViewHolder(ItemviewAudioBinding.inflate(parent.inflater, parent, false), modifier 25 | , viewHolderPlaceholderModifier, viewHolderTitleTextModifier) 26 | holder.itemView.setOnClickListener { 27 | val item = getItem(holder.adapterPosition) 28 | item.isSelected = !item.isSelected 29 | notifyItemChanged(holder.adapterPosition) 30 | } 31 | return holder 32 | } 33 | 34 | override fun onBindViewHolder(holderAudio: AudioMultiSelectViewHolder, position: Int) { 35 | val item = getItem(position) 36 | holderAudio.bind(item) 37 | holderAudio.itemView.tag = item 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/adapters/multi/AudioMultiSelectViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.adapters.multi 2 | 3 | import com.crazylegend.audiopicker.audios.AudioModel 4 | import com.crazylegend.audiopicker.databinding.ItemviewAudioBinding 5 | import com.crazylegend.core.adapters.BaseViewHolder 6 | import com.crazylegend.core.modifiers.TitleTextModifier 7 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 8 | import com.crazylegend.core.modifiers.single.ImageModifier 9 | import com.crazylegend.extensions.visible 10 | 11 | 12 | /** 13 | * Created by crazy on 5/8/20 to long live and prosper ! 14 | */ 15 | internal class AudioMultiSelectViewHolder( 16 | private val binding: ItemviewAudioBinding, 17 | private val modifier: BaseMultiPickerModifier?, 18 | private val viewHolderPlaceholderModifier: ImageModifier?, 19 | private val viewHolderTitleTextModifier: TitleTextModifier? 20 | 21 | ) : BaseViewHolder(binding) { 22 | 23 | private val selectIconModifier get() = modifier?.selectIconModifier 24 | private val unSelectedIconModifier get() = modifier?.unSelectedIconModifier 25 | 26 | init { 27 | binding.selection.visible() 28 | modifier?.applyGravityWithBottomConstraint(binding.selection, binding.bottomText) 29 | } 30 | 31 | fun bind(item: AudioModel) { 32 | binding.bottomText.text = item.displayName 33 | if (item.thumbnail == null) { 34 | loadPlaceHolders(viewHolderPlaceholderModifier, binding.image) 35 | } else { 36 | binding.image.setImageBitmap(item.thumbnail) 37 | } 38 | 39 | viewHolderTitleTextModifier?.apply { 40 | applyTextParamsConstraint(binding.bottomText) 41 | } 42 | 43 | if (item.isSelected) { 44 | setupSelectedImage(binding.selection, selectIconModifier) 45 | } else { 46 | setupUnselectedImage(binding.selection, unSelectedIconModifier) 47 | } 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/adapters/single/AudioSingleAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.adapters.single 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.ListAdapter 5 | import com.crazylegend.audiopicker.adapters.AudioDiffUtil 6 | import com.crazylegend.audiopicker.audios.AudioModel 7 | import com.crazylegend.audiopicker.databinding.ItemviewAudioBinding 8 | import com.crazylegend.core.inflater 9 | import com.crazylegend.core.modifiers.TitleTextModifier 10 | import com.crazylegend.core.modifiers.single.ImageModifier 11 | 12 | 13 | /** 14 | * Created by crazy on 5/8/20 to long live and prosper ! 15 | */ 16 | 17 | internal class AudioSingleAdapter(private val viewHolderPlaceholderModifier: ImageModifier?, 18 | private val viewHolderTitleTextModifier: TitleTextModifier?, 19 | private val onClick: (AudioModel) -> Unit) : ListAdapter(AudioDiffUtil()) { 20 | override fun onCreateViewHolder(parent: ViewGroup, type: Int) = 21 | AudioSingleViewHolder(ItemviewAudioBinding.inflate(parent.inflater, parent, false), onClick, 22 | viewHolderPlaceholderModifier, viewHolderTitleTextModifier) 23 | 24 | override fun onBindViewHolder(holderAudio: AudioSingleViewHolder, position: Int) { 25 | val item = getItem(position) 26 | holderAudio.bind(item) 27 | holderAudio.itemView.tag = item 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/adapters/single/AudioSingleViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.adapters.single 2 | 3 | import com.bumptech.glide.Glide 4 | import com.crazylegend.audiopicker.audios.AudioModel 5 | import com.crazylegend.audiopicker.databinding.ItemviewAudioBinding 6 | import com.crazylegend.core.adapters.BaseViewHolder 7 | import com.crazylegend.core.modifiers.TitleTextModifier 8 | import com.crazylegend.core.modifiers.single.ImageModifier 9 | import com.crazylegend.extensions.gone 10 | 11 | 12 | /** 13 | * Created by crazy on 5/8/20 to long live and prosper ! 14 | */ 15 | 16 | internal class AudioSingleViewHolder(private val binding: ItemviewAudioBinding, onClick: (AudioModel) -> Unit, 17 | private val viewHolderPlaceholderModifier: ImageModifier?, 18 | private val viewHolderTitleTextModifier: TitleTextModifier?) : 19 | BaseViewHolder(binding) { 20 | 21 | 22 | fun bind(item: AudioModel) { 23 | binding.bottomText.text = item.displayName 24 | if (item.thumbnail == null) { 25 | loadPlaceHolders(viewHolderPlaceholderModifier, binding.image) 26 | } else { 27 | Glide 28 | .with(binding.image) 29 | .load(item.thumbnail) 30 | .into(binding.image) 31 | } 32 | viewHolderTitleTextModifier?.apply { 33 | applyTextParamsConstraint(binding.bottomText) 34 | } 35 | } 36 | 37 | private val getModel get() = itemView.tag as? AudioModel? 38 | 39 | init { 40 | itemView.setOnClickListener { 41 | val model = getModel ?: return@setOnClickListener 42 | onClick(model) 43 | } 44 | binding.selection.gone() 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/audios/AudioModel.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.audios 2 | 3 | import android.graphics.Bitmap 4 | import android.net.Uri 5 | import android.os.Parcelable 6 | import com.crazylegend.core.dto.BaseCursorModel 7 | import kotlinx.parcelize.IgnoredOnParcel 8 | import kotlinx.parcelize.Parcelize 9 | 10 | 11 | /** 12 | * Created by crazy on 5/12/20 to long live and prosper ! 13 | */ 14 | 15 | @Parcelize 16 | data class AudioModel(override val id: Long, 17 | override val displayName: String?, 18 | override val dateAdded: Long?, 19 | override val contentUri: Uri, 20 | override val dateModified: Long?, 21 | override val description: String?, 22 | override val size: Int?, 23 | override val width: Int?, 24 | override val height: Int?, 25 | val album: String?, 26 | val composer: String?, 27 | val artist: String?, 28 | val notification: Boolean, 29 | val alarm: Boolean, 30 | val ringtone: Boolean, 31 | val track: Int?, 32 | val year: Int? 33 | ) : 34 | BaseCursorModel(id, displayName, dateAdded, contentUri, dateModified, description, size, width, height), Parcelable { 35 | 36 | /** 37 | * this variable will always be null and/or freed unless after you pick the model and you assign it yourself 38 | * do remember to free the bitmap to not hog the memory 39 | */ 40 | @IgnoredOnParcel 41 | var thumbnail: Bitmap? = null 42 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/consts/DialogConsts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.consts 2 | 3 | 4 | /** 5 | * Created by crazy on 5/8/20 to long live and prosper ! 6 | */ 7 | 8 | 9 | const val SINGLE_PICKER_BOTTOM_SHEET = "audiosSinglePickerBottomSheetDialog" 10 | const val SINGLE_PICKER_DIALOG = "audiosSinglePickerDialog" 11 | 12 | 13 | const val MULTI_PICKER_BOTTOM_SHEET = "audiosMultiPickerBottomSheetDialog" 14 | const val MULTI_PICKER_DIALOG = "audiosMultiPickerDialog" 15 | 16 | -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/contracts/MultiPickerContracts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.contracts 2 | 3 | import com.crazylegend.audiopicker.audios.AudiosVM 4 | import com.crazylegend.audiopicker.listeners.onAudiosPicked 5 | import com.crazylegend.audiopicker.modifiers.MultiAudioPickerModifier 6 | import com.crazylegend.audiopicker.pickers.MultiAudioPicker 7 | import com.crazylegend.core.adapters.multi.MultiSelectAdapter 8 | import com.crazylegend.core.contracts.BaseContractMultiPick 9 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 10 | 11 | 12 | /** 13 | * Created by crazy on 5/12/20 to long live and prosper ! 14 | */ 15 | 16 | internal interface MultiPickerContracts : BaseContractMultiPick { 17 | val audiosVM: AudiosVM 18 | var onAudiosPicked: onAudiosPicked? 19 | val errorTag: String get() = MultiAudioPicker::javaClass.name 20 | fun addModifier(modifier: MultiAudioPickerModifier) 21 | override val modifier: MultiAudioPickerModifier? 22 | fun recycleBitmaps() 23 | override val multiSelectAdapter: MultiSelectAdapter? 24 | get() = null 25 | 26 | override fun addModifier(modifier: BaseMultiPickerModifier) {} 27 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/contracts/SinglePickerContracts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.contracts 2 | 3 | import com.crazylegend.audiopicker.audios.AudiosVM 4 | import com.crazylegend.audiopicker.listeners.onAudioPicked 5 | import com.crazylegend.audiopicker.modifiers.SingleAudioPickerModifier 6 | import com.crazylegend.audiopicker.pickers.SingleAudioPicker 7 | import com.crazylegend.core.adapters.single.SingleAdapter 8 | import com.crazylegend.core.contracts.BaseContractSinglePick 9 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 10 | 11 | 12 | /** 13 | * Created by crazy on 5/12/20 to long live and prosper ! 14 | */ 15 | internal interface SinglePickerContracts : BaseContractSinglePick { 16 | val audiosVM: AudiosVM 17 | var onAudioPicked: onAudioPicked? 18 | val errorTag: String get() = SingleAudioPicker::javaClass.name 19 | 20 | override val modifier: SingleAudioPickerModifier? 21 | override val singleAdapter: SingleAdapter? 22 | get() = null 23 | 24 | override fun addModifier(modifier: BaseSinglePickerModifier) {} 25 | fun recycleBitmaps() 26 | 27 | fun addModifier(modifier: SingleAudioPickerModifier) 28 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/dialogs/multi/MultiAudioPickerBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.dialogs.multi 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import androidx.core.os.bundleOf 12 | import androidx.fragment.app.setFragmentResult 13 | import androidx.fragment.app.viewModels 14 | import androidx.lifecycle.observe 15 | import com.crazylegend.audiopicker.adapters.multi.AudioMultiSelectAdapter 16 | import com.crazylegend.audiopicker.audios.AudiosVM 17 | import com.crazylegend.audiopicker.contracts.MultiPickerContracts 18 | import com.crazylegend.audiopicker.listeners.onAudiosPicked 19 | import com.crazylegend.audiopicker.listeners.recycleBitmapsDSL 20 | import com.crazylegend.audiopicker.modifiers.MultiAudioPickerModifier 21 | import com.crazylegend.audiopicker.pickers.MultiAudioPicker 22 | import com.crazylegend.core.abstracts.AbstractBottomSheetDialogFragment 23 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutMultiBinding 24 | import com.crazylegend.extensions.viewBinding 25 | import com.google.android.material.button.MaterialButton 26 | 27 | 28 | /** 29 | * Created by crazy on 5/8/20 to long live and prosper ! 30 | */ 31 | internal class MultiAudioPickerBottomSheetDialog : AbstractBottomSheetDialogFragment(), 32 | MultiPickerContracts { 33 | 34 | 35 | override val layout: Int 36 | get() = super.layout 37 | override var onAudiosPicked: onAudiosPicked? = null 38 | override val binding by viewBinding(FragmentImagesGalleryLayoutMultiBinding::bind) 39 | override val audiosVM by viewModels() 40 | override val modifier: MultiAudioPickerModifier? 41 | get() = arguments?.getParcelable(modifierTag) 42 | 43 | private val audioMultiSelectAdapter by lazy { 44 | AudioMultiSelectAdapter(modifier, modifier?.viewHolderPlaceholderModifier, modifier?.viewHolderTitleTextModifier) 45 | } 46 | override val askForStoragePermission = 47 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { 48 | if (it) { 49 | audiosVM.loadAudios() 50 | } else { 51 | Log.e(errorTag, "PERMISSION DENIED") 52 | dismissAllowingStateLoss() 53 | } 54 | } 55 | 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 57 | super.onViewCreated(view, savedInstanceState) 58 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU){ 59 | askForStoragePermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE) 60 | } else { 61 | askForStoragePermission.launch(Manifest.permission.READ_MEDIA_AUDIO) 62 | } 63 | setupUIForMultiPicker( 64 | binding.gallery, audioMultiSelectAdapter, binding.doneButton, binding.title, binding.loadingIndicator, modifier?.loadingIndicatorTint, 65 | ::applyDoneButtonModifications, ::applyTitleModifications) 66 | 67 | audiosVM.audio.observe(viewLifecycleOwner) { 68 | setupList(audioMultiSelectAdapter, it, binding.noContentText, modifier?.noContentTextModifier) 69 | } 70 | 71 | binding.doneButton.setOnClickListener { 72 | val selectedList = audioMultiSelectAdapter.currentList.filter { it.isSelected } 73 | setFragmentResult(MultiAudioPicker.MULTI_AUDIO_REQUEST_KEY, bundleOf(MultiAudioPicker.ON_MULTI_AUDIO_PICK_KEY to selectedList)) 74 | onAudiosPicked?.forAudios(selectedList) 75 | dismissAllowingStateLoss() 76 | } 77 | 78 | handleUIIndicator(audiosVM.loadingIndicator, binding.loadingIndicator) 79 | audiosVM.onShouldRecycleBitmaps = recycleBitmapsDSL { 80 | recycleBitmaps() 81 | } 82 | } 83 | 84 | 85 | override fun onDestroyView() { 86 | super.onDestroyView() 87 | onAudiosPicked = null 88 | } 89 | 90 | override fun addModifier(modifier: MultiAudioPickerModifier) { 91 | arguments = bundleOf(modifierTag to modifier) 92 | } 93 | 94 | override fun applyTitleModifications(appCompatTextView: AppCompatTextView) { 95 | modifier?.titleTextModifier?.applyTextParams(appCompatTextView) 96 | } 97 | 98 | override fun applyDoneButtonModifications(doneButton: MaterialButton) { 99 | modifier?.doneButtonModifier?.applyImageParams(doneButton) 100 | } 101 | 102 | override fun recycleBitmaps() { 103 | audioMultiSelectAdapter.currentList.asSequence().forEach { 104 | it?.thumbnail?.apply { 105 | if (!isRecycled) 106 | recycle() 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/dialogs/single/SingleAudioPickerBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.dialogs.single 2 | 3 | import android.Manifest 4 | import android.graphics.Bitmap 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.view.View 9 | 10 | import androidx.activity.result.contract.ActivityResultContracts 11 | import androidx.appcompat.widget.AppCompatTextView 12 | import androidx.core.os.bundleOf 13 | import androidx.fragment.app.setFragmentResult 14 | import androidx.fragment.app.viewModels 15 | import androidx.lifecycle.observe 16 | import com.crazylegend.audiopicker.adapters.single.AudioSingleAdapter 17 | import com.crazylegend.audiopicker.audios.AudiosVM 18 | import com.crazylegend.audiopicker.contracts.SinglePickerContracts 19 | import com.crazylegend.audiopicker.listeners.onAudioPicked 20 | import com.crazylegend.audiopicker.listeners.recycleBitmapsDSL 21 | import com.crazylegend.audiopicker.modifiers.SingleAudioPickerModifier 22 | import com.crazylegend.audiopicker.pickers.SingleAudioPicker 23 | import com.crazylegend.core.abstracts.AbstractBottomSheetDialogFragment 24 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutBinding 25 | import com.crazylegend.extensions.viewBinding 26 | 27 | 28 | /** 29 | * Created by crazy on 5/8/20 to long live and prosper ! 30 | */ 31 | internal class SingleAudioPickerBottomSheetDialog : AbstractBottomSheetDialogFragment(), SinglePickerContracts { 32 | 33 | override val layout: Int 34 | get() = super.layout 35 | override var onAudioPicked: onAudioPicked? = null 36 | override val binding by viewBinding(FragmentImagesGalleryLayoutBinding::bind) 37 | override val audiosVM by viewModels() 38 | override val modifier: SingleAudioPickerModifier? get() = arguments?.getParcelable(modifierTag) 39 | 40 | private val singleAudioAdapter by lazy { 41 | AudioSingleAdapter(modifier?.viewHolderPlaceholderModifier, modifier?.viewHolderTitleTextModifier) { 42 | recycleThubmnail(it.thumbnail) 43 | setFragmentResult(SingleAudioPicker.SINGLE_AUDIO_REQUEST_KEY, bundleOf(SingleAudioPicker.ON_SINGLE_AUDIO_PICK_KEY to it)) 44 | onAudioPicked?.forAudio(it) 45 | dismissAllowingStateLoss() 46 | } 47 | } 48 | 49 | override val askForStoragePermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { 50 | if (it) { 51 | audiosVM.loadAudios() 52 | } else { 53 | Log.e(errorTag, "PERMISSION DENIED") 54 | dismissAllowingStateLoss() 55 | } 56 | } 57 | 58 | 59 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 60 | super.onViewCreated(view, savedInstanceState) 61 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU){ 62 | askForStoragePermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE) 63 | } else { 64 | askForStoragePermission.launch(Manifest.permission.READ_MEDIA_AUDIO) 65 | } 66 | setupUIForSinglePicker(binding.gallery, singleAudioAdapter, binding.title, binding.loadingIndicator, modifier?.loadingIndicatorTint, ::applyTitleModifications) 67 | audiosVM.audio.observe(viewLifecycleOwner) { 68 | setupList(singleAudioAdapter, it, binding.noContentText, modifier?.noContentTextModifier) 69 | } 70 | 71 | handleUIIndicator(audiosVM.loadingIndicator, binding.loadingIndicator) 72 | audiosVM.onShouldRecycleBitmaps = recycleBitmapsDSL { 73 | recycleBitmaps() 74 | } 75 | } 76 | 77 | override fun onDestroyView() { 78 | super.onDestroyView() 79 | onAudioPicked = null 80 | } 81 | 82 | 83 | override fun applyTitleModifications(appCompatTextView: AppCompatTextView) { 84 | modifier?.titleTextModifier?.applyTextParams(appCompatTextView) 85 | } 86 | 87 | override fun addModifier(modifier: SingleAudioPickerModifier) { 88 | arguments = bundleOf(modifierTag to modifier) 89 | } 90 | 91 | override fun recycleBitmaps() { 92 | singleAudioAdapter.currentList.asSequence().forEach { 93 | it?.thumbnail?.apply { 94 | if (!isRecycled) 95 | recycle() 96 | } 97 | } 98 | } 99 | 100 | private fun recycleThubmnail(thumbnail: Bitmap?) { 101 | thumbnail?.apply { 102 | if (!isRecycled) { 103 | recycle() 104 | } 105 | } 106 | } 107 | 108 | 109 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/listeners/onAudioPicked.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.listeners 2 | 3 | import com.crazylegend.audiopicker.audios.AudioModel 4 | 5 | 6 | /** 7 | * Created by crazy on 5/12/20 to long live and prosper ! 8 | */ 9 | internal fun interface onAudioPicked { 10 | fun forAudio(audio: AudioModel) 11 | } 12 | 13 | internal inline fun onAudioDSL(crossinline callback: (audioModel: AudioModel) -> Unit = {}) = onAudioPicked { audio -> callback(audio) } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/listeners/onAudiosPicked.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.listeners 2 | 3 | import com.crazylegend.audiopicker.audios.AudioModel 4 | 5 | 6 | /** 7 | * Created by crazy on 5/12/20 to long live and prosper ! 8 | */ 9 | internal fun interface onAudiosPicked { 10 | fun forAudios(audioList: List) 11 | } 12 | 13 | internal inline fun onAudiosDSL(crossinline callback: (audioList: List) -> Unit = {}) = onAudiosPicked { audioList -> callback(audioList) } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/listeners/onShouldRecycleBitmaps.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.listeners 2 | 3 | 4 | /** 5 | * Created by crazy on 5/15/20 to long live and prosper ! 6 | */ 7 | internal interface onShouldRecycleBitmaps { 8 | fun keepItClean() 9 | } 10 | 11 | internal inline fun recycleBitmapsDSL(crossinline recycle: () -> Unit = {}) = object : onShouldRecycleBitmaps { 12 | override fun keepItClean() { 13 | recycle() 14 | } 15 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/modifiers/MultiAudioPickerModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.modifiers 2 | 3 | import com.crazylegend.core.modifiers.TitleTextModifier 4 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 5 | import com.crazylegend.core.modifiers.multi.DoneButtonModifier 6 | import com.crazylegend.core.modifiers.multi.SelectIconModifier 7 | import com.crazylegend.core.modifiers.single.ImageModifier 8 | import kotlinx.parcelize.Parcelize 9 | 10 | 11 | /** 12 | * Created by crazy on 5/14/20 to long live and prosper ! 13 | */ 14 | 15 | @Parcelize 16 | data class MultiAudioPickerModifier( 17 | override val doneButtonModifier: DoneButtonModifier = DoneButtonModifier(), 18 | override val titleTextModifier: TitleTextModifier = TitleTextModifier(), 19 | override val selectIconModifier: SelectIconModifier = SelectIconModifier(), 20 | override val unSelectedIconModifier: SelectIconModifier = SelectIconModifier(), 21 | override var indicatorsGravity: Gravity = Gravity.BOTTOM_RIGHT, 22 | override val viewHolderPlaceholderModifier: ImageModifier = ImageModifier(), 23 | override val noContentTextModifier: TitleTextModifier = TitleTextModifier(), 24 | override var loadingIndicatorTint: Int? = null, 25 | 26 | val viewHolderTitleTextModifier: TitleTextModifier = TitleTextModifier() 27 | ) : BaseMultiPickerModifier(doneButtonModifier, titleTextModifier, selectIconModifier, unSelectedIconModifier, indicatorsGravity, viewHolderPlaceholderModifier, 28 | noContentTextModifier, loadingIndicatorTint) { 29 | 30 | inline fun setupViewHolderTitleText(viewHolderPlaceholderModifications: TitleTextModifier.() -> Unit = {}) { 31 | viewHolderTitleTextModifier.viewHolderPlaceholderModifications() 32 | } 33 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/modifiers/SingleAudioPickerModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.modifiers 2 | 3 | import com.crazylegend.core.modifiers.TitleTextModifier 4 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 5 | import com.crazylegend.core.modifiers.single.ImageModifier 6 | import kotlinx.parcelize.Parcelize 7 | 8 | 9 | /** 10 | * Created by crazy on 5/14/20 to long live and prosper ! 11 | */ 12 | 13 | @Parcelize 14 | data class SingleAudioPickerModifier( 15 | override val viewHolderPlaceholderModifier: ImageModifier = ImageModifier(), 16 | override val titleTextModifier: TitleTextModifier = TitleTextModifier(), 17 | override var loadingIndicatorTint: Int? = null, 18 | override val noContentTextModifier: TitleTextModifier = TitleTextModifier(), 19 | val viewHolderTitleTextModifier: TitleTextModifier = TitleTextModifier() 20 | ) : BaseSinglePickerModifier(titleTextModifier, viewHolderPlaceholderModifier, noContentTextModifier, loadingIndicatorTint) { 21 | 22 | inline fun setupViewHolderTitleText(viewHolderPlaceholderModifications: TitleTextModifier.() -> Unit = {}) { 23 | viewHolderTitleTextModifier.viewHolderPlaceholderModifications() 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/pickers/MultiAudioPicker.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.pickers 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.util.Log 6 | import androidx.annotation.RequiresPermission 7 | import androidx.fragment.app.FragmentManager 8 | import com.crazylegend.audiopicker.audios.AudioModel 9 | import com.crazylegend.audiopicker.consts.MULTI_PICKER_BOTTOM_SHEET 10 | import com.crazylegend.audiopicker.consts.MULTI_PICKER_DIALOG 11 | import com.crazylegend.audiopicker.dialogs.multi.MultiAudioPickerBottomSheetDialog 12 | import com.crazylegend.audiopicker.listeners.onAudiosDSL 13 | import com.crazylegend.audiopicker.modifiers.MultiAudioPickerModifier 14 | import com.crazylegend.extensions.setupManager 15 | 16 | 17 | /** 18 | * Created by crazy on 5/12/20 to long live and prosper ! 19 | */ 20 | object MultiAudioPicker { 21 | 22 | 23 | const val MULTI_AUDIO_REQUEST_KEY = "multiAudiosRequest" 24 | const val ON_MULTI_AUDIO_PICK_KEY = "onMultiAudiosPicked" 25 | 26 | fun restoreListener(context: Context, audioList: (list: List) -> Unit = {}) { 27 | val manager = context.setupManager() 28 | when (val fragment = manager.findFragmentByTag(MULTI_PICKER_BOTTOM_SHEET) 29 | ?: manager.findFragmentByTag(MULTI_PICKER_DIALOG)) { 30 | is MultiAudioPickerBottomSheetDialog -> { 31 | fragment.onAudiosPicked = onAudiosDSL(audioList) 32 | } 33 | null -> { 34 | Log.e(MultiAudioPicker::class.java.name, "FRAGMENT NOT FOUND") 35 | } 36 | } 37 | } 38 | 39 | 40 | fun showPicker(context: Context, modifier: MultiAudioPickerModifier.() -> Unit = {}, audioList: (list: List) -> Unit = {}) { 41 | val manager = context.setupManager() 42 | val setupModifier = setupModifier(modifier) 43 | with(MultiAudioPickerBottomSheetDialog()) { 44 | addModifier(setupModifier) 45 | onAudiosPicked = onAudiosDSL(audioList) 46 | show(manager, MULTI_PICKER_BOTTOM_SHEET) 47 | } 48 | } 49 | 50 | fun showPicker(fragmentManager: FragmentManager, modifier: MultiAudioPickerModifier.() -> Unit = {}, audioList: (list: List) -> Unit = {}) { 51 | val setupModifier = setupModifier(modifier) 52 | with(MultiAudioPickerBottomSheetDialog()) { 53 | addModifier(setupModifier) 54 | onAudiosPicked = onAudiosDSL(audioList) 55 | show(fragmentManager, MULTI_PICKER_BOTTOM_SHEET) 56 | } 57 | } 58 | 59 | private inline fun setupModifier(modifier: MultiAudioPickerModifier.() -> Unit) = 60 | MultiAudioPickerModifier().also(modifier) 61 | } -------------------------------------------------------------------------------- /audiopicker/src/main/java/com/crazylegend/audiopicker/pickers/SingleAudioPicker.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker.pickers 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.util.Log 6 | import androidx.annotation.RequiresPermission 7 | import androidx.fragment.app.FragmentManager 8 | import com.crazylegend.audiopicker.audios.AudioModel 9 | import com.crazylegend.audiopicker.consts.SINGLE_PICKER_BOTTOM_SHEET 10 | import com.crazylegend.audiopicker.consts.SINGLE_PICKER_DIALOG 11 | import com.crazylegend.audiopicker.dialogs.single.SingleAudioPickerBottomSheetDialog 12 | import com.crazylegend.audiopicker.listeners.onAudioDSL 13 | import com.crazylegend.audiopicker.modifiers.SingleAudioPickerModifier 14 | import com.crazylegend.extensions.setupManager 15 | 16 | 17 | /** 18 | * Created by crazy on 5/12/20 to long live and prosper ! 19 | */ 20 | object SingleAudioPicker { 21 | 22 | const val SINGLE_AUDIO_REQUEST_KEY = "singleAudioRequest" 23 | const val ON_SINGLE_AUDIO_PICK_KEY = "onSingleAudioPicked" 24 | 25 | fun restoreListener(context: Context, onPickedAudio: (audio: AudioModel) -> Unit = {}) { 26 | val manager = context.setupManager() 27 | when (val fragment = manager.findFragmentByTag(SINGLE_PICKER_BOTTOM_SHEET) 28 | ?: manager.findFragmentByTag(SINGLE_PICKER_DIALOG)) { 29 | is SingleAudioPickerBottomSheetDialog -> { 30 | fragment.onAudioPicked = onAudioDSL(onPickedAudio) 31 | } 32 | null -> { 33 | Log.e(SingleAudioPicker::class.java.name, "FRAGMENT NOT FOUND") 34 | } 35 | } 36 | } 37 | 38 | fun showPicker(context: Context, pickerModifier: SingleAudioPickerModifier.() -> Unit = {}, onPickedAudio: (audio: AudioModel) -> Unit = {}) { 39 | val modifier = setupModifier(pickerModifier) 40 | val manager = context.setupManager() 41 | with(SingleAudioPickerBottomSheetDialog()) { 42 | addModifier(modifier) 43 | onAudioPicked = onAudioDSL(onPickedAudio) 44 | show(manager, SINGLE_PICKER_BOTTOM_SHEET) 45 | } 46 | } 47 | 48 | fun showPicker(manager: FragmentManager, pickerModifier: SingleAudioPickerModifier.() -> Unit = {}, onPickedAudio: (audio: AudioModel) -> Unit = {}) { 49 | val modifier = setupModifier(pickerModifier) 50 | with(SingleAudioPickerBottomSheetDialog()) { 51 | addModifier(modifier) 52 | onAudioPicked = onAudioDSL(onPickedAudio) 53 | show(manager, SINGLE_PICKER_BOTTOM_SHEET) 54 | } 55 | } 56 | 57 | private inline fun setupModifier(audioPicker: SingleAudioPickerModifier.() -> Unit) = SingleAudioPickerModifier().also(audioPicker) 58 | } -------------------------------------------------------------------------------- /audiopicker/src/main/res/drawable/ic_album.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /audiopicker/src/main/res/drawable/ic_album_second.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /audiopicker/src/main/res/layout/itemview_audio.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 20 | 21 | 29 | 30 | 40 | 41 | -------------------------------------------------------------------------------- /audiopicker/src/test/java/com/crazylegend/audiopicker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.audiopicker 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | lifecycle = '2.3.0' 3 | glide = "4.13.2" 4 | constraint_layout = '2.1.4' 5 | material = "1.3.0" 6 | fragment = "1.5.0" 7 | activity = "1.2.0" 8 | recycler = "1.2.1" 9 | appCompat = "1.2.0" 10 | coreKTX = "1.5.0" 11 | 12 | //compilation 13 | compileVersion = 33 14 | minVersion = 21 15 | verCode = 1 16 | verName = "1.0.0" 17 | testRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildscript { 21 | ext.kotlin_version = '1.7.10' 22 | 23 | ext.compiledAppVersion = 33 24 | 25 | repositories { 26 | google() 27 | mavenCentral() 28 | } 29 | dependencies { 30 | classpath 'com.android.tools.build:gradle:7.2.1' 31 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 32 | } 33 | } 34 | 35 | allprojects { 36 | group = "io.github.funkymuse" 37 | 38 | repositories { 39 | google() 40 | mavenCentral() 41 | } 42 | } 43 | 44 | task clean(type: Delete) { 45 | delete rootProject.buildDir 46 | } 47 | 48 | 49 | 50 | subprojects { 51 | 52 | switch (it.name){ 53 | case 'app': 54 | apply plugin: 'com.android.application' 55 | apply plugin: 'kotlin-android' 56 | apply plugin: 'kotlin-kapt' 57 | apply plugin: 'kotlin-parcelize' 58 | 59 | dependencies { 60 | implementation project(path: ':imagepicker') 61 | implementation project(path: ':videopicker') 62 | implementation project(path: ':audiopicker') 63 | implementation project(path: ':core') 64 | } 65 | 66 | break 67 | 68 | case 'core': 69 | 70 | apply plugin: 'com.android.library' 71 | apply plugin: 'kotlin-android' 72 | apply plugin: 'kotlin-parcelize' 73 | apply plugin: 'kotlin-kapt' 74 | apply plugin: 'maven-publish' 75 | 76 | 77 | dependencies { 78 | testImplementation 'junit:junit:4.13.2' 79 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 80 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 81 | 82 | //glide 83 | api "com.github.bumptech.glide:glide:$glide" 84 | kapt "com.github.bumptech.glide:compiler:$glide" 85 | 86 | //core 87 | api "androidx.activity:activity-ktx:$activity" 88 | api "androidx.fragment:fragment-ktx:$fragment" 89 | api "androidx.appcompat:appcompat:$appCompat" 90 | api "androidx.core:core-ktx:$coreKTX" 91 | 92 | //ui 93 | api "androidx.constraintlayout:constraintlayout:$constraint_layout" 94 | api "androidx.recyclerview:recyclerview:$recycler" 95 | api "com.google.android.material:material:$material" 96 | 97 | //live data 98 | api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle" 99 | api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle" 100 | api "androidx.lifecycle:lifecycle-common-java8:$lifecycle" 101 | api "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle" 102 | } 103 | 104 | it.afterEvaluate { 105 | publishing { 106 | publications { 107 | release(MavenPublication) { 108 | from components.release 109 | } 110 | } 111 | } 112 | } 113 | 114 | break 115 | 116 | case 'extensions': 117 | 118 | 119 | apply plugin: 'com.android.library' 120 | apply plugin: 'kotlin-android' 121 | apply plugin: 'kotlin-parcelize' 122 | apply plugin: 'kotlin-kapt' 123 | apply plugin: 'maven-publish' 124 | 125 | dependencies { 126 | implementation project(path: ':core') 127 | } 128 | it.afterEvaluate { 129 | publishing { 130 | publications { 131 | release(MavenPublication) { 132 | from components.release 133 | } 134 | } 135 | } 136 | } 137 | 138 | break 139 | 140 | default: 141 | 142 | apply plugin: 'com.android.library' 143 | apply plugin: 'kotlin-android' 144 | apply plugin: 'kotlin-parcelize' 145 | apply plugin: 'kotlin-kapt' 146 | apply plugin: 'maven-publish' 147 | 148 | dependencies { 149 | api project(path: ':core') 150 | implementation project(path: ':extensions') 151 | } 152 | 153 | it.afterEvaluate { 154 | publishing { 155 | publications { 156 | release(MavenPublication) { 157 | from components.release 158 | } 159 | } 160 | } 161 | } 162 | 163 | break 164 | 165 | } 166 | 167 | android { 168 | compileSdkVersion compileVersion 169 | 170 | defaultConfig { 171 | minSdkVersion minVersion 172 | targetSdkVersion compileVersion 173 | versionCode verCode 174 | versionName verName 175 | testInstrumentationRunner testRunner 176 | } 177 | 178 | compileOptions { 179 | sourceCompatibility = 1.8 180 | targetCompatibility = 1.8 181 | } 182 | 183 | kotlinOptions { 184 | jvmTarget = "1.8" 185 | } 186 | 187 | buildFeatures { 188 | viewBinding = true 189 | aidl = false 190 | renderScript = false 191 | resValues = false 192 | shaders = false 193 | buildConfig = false 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | android { 2 | namespace 'com.crazylegend.core' 3 | } -------------------------------------------------------------------------------- /core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/core/consumer-rules.pro -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/crazylegend/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.crazylegend.core.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.drawable.Drawable 6 | import android.net.Uri 7 | import android.util.TypedValue 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.appcompat.widget.AppCompatImageView 12 | import androidx.constraintlayout.widget.ConstraintLayout 13 | import androidx.constraintlayout.widget.ConstraintSet 14 | import androidx.lifecycle.AndroidViewModel 15 | import com.bumptech.glide.Glide 16 | import com.bumptech.glide.load.DataSource 17 | import com.bumptech.glide.load.engine.GlideException 18 | import com.bumptech.glide.request.RequestListener 19 | import com.bumptech.glide.request.target.Target 20 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 21 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 22 | import kotlin.math.pow 23 | 24 | /** 25 | * Created by crazy on 5/8/20 to long live and prosper ! 26 | */ 27 | 28 | 29 | internal inline fun AppCompatImageView.loadImage(uri: Uri, crossinline onLoadFailed: () -> Unit = {}) { 30 | Glide.with(this) 31 | .load(uri) 32 | .thumbnail(0.33f) 33 | .listener(object : RequestListener { 34 | override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { 35 | onLoadFailed() 36 | return true 37 | } 38 | 39 | override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { 40 | return false 41 | } 42 | }) 43 | .centerCrop() 44 | .into(this) 45 | } 46 | 47 | internal inline fun AppCompatImageView.loadWholeImage(uri: Uri, crossinline onLoadFailed: () -> Unit = {}) { 48 | Glide.with(this) 49 | .load(uri) 50 | .listener(object : RequestListener { 51 | override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { 52 | onLoadFailed() 53 | return true 54 | } 55 | 56 | override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { 57 | return false 58 | } 59 | }) 60 | .centerCrop() 61 | .into(this) 62 | } 63 | 64 | inline fun setupModifier(modifier: BaseMultiPickerModifier.() -> Unit) = BaseMultiPickerModifier().also(modifier) 65 | 66 | 67 | inline fun setupModifier(videoPicker: BaseSinglePickerModifier.() -> Unit) = BaseSinglePickerModifier().also(videoPicker) 68 | 69 | 70 | val ViewGroup.inflater: LayoutInflater get() = LayoutInflater.from(context) 71 | 72 | 73 | internal fun View.visible() { 74 | this.visibility = View.VISIBLE 75 | } 76 | 77 | internal fun View.gone() { 78 | this.visibility = View.GONE 79 | } 80 | 81 | internal val AndroidViewModel.context: Context 82 | get() = getApplication() 83 | 84 | 85 | 86 | internal fun T.top(margin: Int): T { 87 | return position(ConstraintLayout.LayoutParams.TOP, margin) 88 | } 89 | 90 | internal fun T.left(margin: Int): T { 91 | return position(ConstraintLayout.LayoutParams.LEFT, margin) 92 | } 93 | 94 | internal fun T.right(margin: Int): T { 95 | return position(ConstraintLayout.LayoutParams.RIGHT, margin) 96 | } 97 | 98 | internal fun T.bottom(margin: Int): T { 99 | return position(ConstraintLayout.LayoutParams.BOTTOM, margin) 100 | } 101 | 102 | internal fun T.position(position: Int, margin: Int): T { 103 | (parent as? ConstraintLayout)?.let { constraintLayout -> 104 | constraintLayout.addConstraints { 105 | connect(id, position, ConstraintLayout.LayoutParams.PARENT_ID, position, margin.dp) 106 | } 107 | } 108 | return this 109 | } 110 | 111 | // Bottom 112 | 113 | internal fun T.constrainBottomToTopOf(view: View, margin: Int = 0): T { 114 | return constrainBottomToTopOf(view.id, margin) 115 | } 116 | 117 | internal fun T.constrainBottomToTopOf(viewId: Int, margin: Int = 0): T { 118 | (parent as? ConstraintLayout)?.addConstraints { 119 | connect(id, ConstraintLayout.LayoutParams.BOTTOM, viewId, ConstraintLayout.LayoutParams.TOP, margin.dp) 120 | } 121 | return this 122 | } 123 | 124 | internal fun ConstraintLayout.addConstraints(block: ConstraintSet.() -> Unit) { 125 | val cs = ConstraintSet() 126 | cs.clone(this) 127 | block(cs) 128 | cs.applyTo(this) 129 | } 130 | 131 | 132 | internal var Int.dp: Int 133 | get() { 134 | val metrics = Resources.getSystem().displayMetrics 135 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), metrics).toInt() 136 | } 137 | set(_) {} 138 | 139 | 140 | internal var Float.dp: Float 141 | get() { 142 | val metrics = Resources.getSystem().displayMetrics 143 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, metrics) 144 | } 145 | set(_) {} 146 | 147 | internal fun Int.bytesToFormattedString(): String { 148 | return when { 149 | this >= 1024.0.pow(3.0) -> { 150 | val gigabytes = this / 1024.0.pow(3.0) 151 | "%.2f GB".format(gigabytes) 152 | } 153 | else -> { 154 | val megabytes = this / 1024.0.pow(2.0) 155 | "%.2f MB".format(megabytes) 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/abstracts/AbstractAVM.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.abstracts 2 | 3 | import android.app.Application 4 | import android.content.ContentResolver 5 | import android.database.ContentObserver 6 | import android.graphics.Bitmap 7 | import android.graphics.BitmapFactory 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.provider.MediaStore 11 | import android.util.Size 12 | import androidx.lifecycle.AndroidViewModel 13 | import androidx.lifecycle.LiveData 14 | import androidx.lifecycle.MutableLiveData 15 | import androidx.lifecycle.SavedStateHandle 16 | import com.crazylegend.core.context 17 | 18 | 19 | /** 20 | * Created by crazy on 5/14/20 to long live and prosper ! 21 | */ 22 | abstract class AbstractAVM(application: Application, 23 | private val stateHandle: SavedStateHandle) : AndroidViewModel(application) { 24 | 25 | companion object { 26 | private const val CAN_LOAD_KEY = "canLoad" 27 | } 28 | 29 | protected val contentResolver get() = context.contentResolver 30 | protected var contentObserver: ContentObserver? = null 31 | 32 | protected val loadingIndicatorData = MutableLiveData() 33 | val loadingIndicator: LiveData = loadingIndicatorData 34 | 35 | /** 36 | * Using this instead of event since it serves the same purpose thus it's needed here 37 | */ 38 | protected val canLoad get() = stateHandle[CAN_LOAD_KEY] ?: true 39 | 40 | protected fun setCanLoad() { 41 | stateHandle[CAN_LOAD_KEY] = true 42 | } 43 | 44 | protected fun setCanNotLoad() { 45 | stateHandle[CAN_LOAD_KEY] = false 46 | } 47 | 48 | override fun onCleared() { 49 | super.onCleared() 50 | contentObserver?.apply { 51 | contentResolver.unregisterContentObserver(this) 52 | } 53 | } 54 | 55 | 56 | /** 57 | * Or just use Glide with the contentUri 58 | * @param contentResolver ContentResolver 59 | * @param customSize Size 60 | * @param options Options? 61 | * @return Bitmap? 62 | */ 63 | @Suppress("DEPRECATION") 64 | fun loadThumbnail(contentResolver: ContentResolver, 65 | contentUri: Uri, 66 | id: Long, 67 | customSize: Size = Size(350, 350), 68 | legacyKind: Int = MediaStore.Video.Thumbnails.MICRO_KIND, 69 | options: BitmapFactory.Options? = null): Bitmap? { 70 | return tryOrNull { 71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 72 | contentResolver.loadThumbnail(contentUri, customSize, null) 73 | } else { 74 | MediaStore.Video.Thumbnails.getThumbnail(contentResolver, id, legacyKind, 75 | options ?: BitmapFactory.Options()) 76 | } 77 | } 78 | } 79 | 80 | private fun tryOrNull(function: () -> Bitmap): Bitmap? { 81 | return try { 82 | function() 83 | } catch (e: Exception) { 84 | null 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/abstracts/AbstractBottomSheetDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.abstracts 2 | 3 | import android.content.res.Configuration 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.ProgressBar 9 | import androidx.lifecycle.LiveData 10 | import androidx.recyclerview.widget.GridLayoutManager 11 | import androidx.recyclerview.widget.ListAdapter 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.crazylegend.core.R 14 | import com.crazylegend.core.gone 15 | import com.crazylegend.core.modifiers.TitleTextModifier 16 | import com.crazylegend.core.visible 17 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 18 | import com.google.android.material.button.MaterialButton 19 | import com.google.android.material.textview.MaterialTextView 20 | 21 | 22 | /** 23 | * Created by crazy on 5/9/20 to long live and prosper ! 24 | */ 25 | abstract class AbstractBottomSheetDialogFragment : BottomSheetDialogFragment() { 26 | abstract val layout: Int 27 | 28 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(layout, container, false) 29 | 30 | //work around when using DayNight to change the background color to light/night 31 | override fun onConfigurationChanged(overrideConfiguration: Configuration) { 32 | val uiMode = overrideConfiguration.uiMode 33 | overrideConfiguration.setTo(requireContext().resources.configuration) 34 | overrideConfiguration.uiMode = uiMode 35 | super.onConfigurationChanged(overrideConfiguration) 36 | } 37 | 38 | 39 | fun setupUIForSinglePicker(gallery: RecyclerView, singleAdapter: RecyclerView.Adapter<*>, 40 | title: MaterialTextView, loadingIndicator: ProgressBar, progressBarTint: Int?, 41 | titleModifications: (MaterialTextView) -> Unit) { 42 | gallery.apply { 43 | layoutManager = GridLayoutManager(requireContext(), 3) 44 | adapter = singleAdapter 45 | } 46 | titleModifications(title) 47 | progressBarTint?.let { loadingIndicator.indeterminateDrawable.setTint(it) } 48 | } 49 | 50 | fun setupList(adapter: ListAdapter, list: List, noContentText: MaterialTextView, noContentTextModifier: TitleTextModifier?) { 51 | if (list.isEmpty()) { 52 | adapter.submitList(emptyList()) 53 | noContentText.visible() 54 | noContentTextModifier?.apply { 55 | textString = textString ?: getString(R.string.no_content_found) 56 | applyTextParams(noContentText) 57 | } 58 | } else { 59 | noContentText.gone() 60 | adapter.submitList(list) 61 | } 62 | } 63 | 64 | fun setupUIForMultiPicker(gallery: RecyclerView, 65 | multiSelectAdapter: RecyclerView.Adapter<*>, doneButton: MaterialButton, 66 | title: MaterialTextView, loadingIndicator: ProgressBar, progressBarTint: Int?, onDoneButton: (MaterialButton) -> Unit, 67 | onTitleButton: (MaterialTextView) -> Unit) { 68 | gallery.apply { 69 | layoutManager = GridLayoutManager(requireContext(), 3) 70 | adapter = multiSelectAdapter 71 | } 72 | onDoneButton(doneButton) 73 | onTitleButton(title) 74 | progressBarTint?.let { loadingIndicator.indeterminateDrawable.setTint(it) } 75 | } 76 | 77 | fun handleUIIndicator(liveData: LiveData, loadingIndicator: ProgressBar) { 78 | liveData.observe(viewLifecycleOwner) { 79 | if (it) { 80 | loadingIndicator.visible() 81 | } else { 82 | loadingIndicator.gone() 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/adapters/BaseViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.adapters 2 | 3 | import android.content.ContentResolver 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.provider.MediaStore 9 | import android.util.Size 10 | import androidx.appcompat.widget.AppCompatImageView 11 | import androidx.recyclerview.widget.RecyclerView 12 | import androidx.viewbinding.ViewBinding 13 | import com.crazylegend.core.R 14 | import com.crazylegend.core.loadImage 15 | import com.crazylegend.core.modifiers.multi.SelectIconModifier 16 | import com.crazylegend.core.modifiers.single.ImageModifier 17 | 18 | 19 | /** 20 | * Created by crazy on 5/16/20 to long live and prosper ! 21 | */ 22 | open class BaseViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) { 23 | 24 | fun loadImage(image: AppCompatImageView, contentUri: Uri, viewHolderPlaceholderModifier: ImageModifier?) { 25 | image.loadImage(contentUri) { 26 | if (viewHolderPlaceholderModifier != null) { 27 | loadPlaceHolders(viewHolderPlaceholderModifier, image) 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * Or just use Glide with the contentUri 34 | * @param contentResolver ContentResolver 35 | * @param customSize Size 36 | * @param options Options? 37 | * @return Bitmap? 38 | */ 39 | @Suppress("DEPRECATION") 40 | fun loadThumbnail(contentResolver: ContentResolver, 41 | contentUri: Uri, 42 | id: Long, 43 | customSize: Size = Size(350, 350), 44 | legacyKind: Int = MediaStore.Video.Thumbnails.MICRO_KIND, 45 | options: BitmapFactory.Options? = null): Bitmap? { 46 | return tryOrNull { 47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 48 | contentResolver.loadThumbnail(contentUri, customSize, null) 49 | } else { 50 | MediaStore.Video.Thumbnails.getThumbnail(contentResolver, id, legacyKind, 51 | options ?: BitmapFactory.Options()) 52 | } 53 | } 54 | } 55 | 56 | private fun tryOrNull(function: () -> Bitmap): Bitmap? { 57 | return try { 58 | function() 59 | } catch (e: Exception) { 60 | null 61 | } 62 | } 63 | 64 | fun loadPlaceHolders(viewHolderPlaceholderModifier: ImageModifier?, image: AppCompatImageView) { 65 | if (viewHolderPlaceholderModifier != null) { 66 | viewHolderPlaceholderModifier.resID = viewHolderPlaceholderModifier.resID 67 | ?: R.drawable.ic_image 68 | viewHolderPlaceholderModifier.applyImageParamsConstraintLayout(image) 69 | } else { 70 | image.setImageResource(R.drawable.ic_image) 71 | } 72 | } 73 | 74 | fun setupUnselectedImage(selection: AppCompatImageView, unSelectedIconModifier: SelectIconModifier?) { 75 | val resID = unSelectedIconModifier?.resID ?: R.drawable.ic_unchecked_default 76 | unSelectedIconModifier?.applyImageParams(selection, resID) 77 | } 78 | 79 | fun setupSelectedImage(selection: AppCompatImageView, selectIconModifier: SelectIconModifier?) { 80 | val resID = selectIconModifier?.resID ?: R.drawable.ic_checked_default 81 | selectIconModifier?.applyImageParams(selection, resID) 82 | } 83 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/adapters/multi/MultiSelectAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.adapters.multi 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.ListAdapter 5 | import com.crazylegend.core.adapters.single.SingleDiffUtil 6 | import com.crazylegend.core.databinding.ItemviewImageBinding 7 | import com.crazylegend.core.dto.BaseCursorModel 8 | import com.crazylegend.core.inflater 9 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 10 | 11 | 12 | /** 13 | * Created by crazy on 5/8/20 to long live and prosper ! 14 | */ 15 | class MultiSelectAdapter(private val modifier: BaseMultiPickerModifier?, private val showFileSize: Boolean) : 16 | ListAdapter(SingleDiffUtil()) { 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiSelectViewHolder { 19 | val holder = MultiSelectViewHolder(ItemviewImageBinding.inflate(parent.inflater, parent, false), modifier) 20 | holder.itemView.setOnClickListener { 21 | val item = getItem(holder.bindingAdapterPosition) 22 | item.isSelected = !item.isSelected 23 | notifyItemChanged(holder.bindingAdapterPosition) 24 | } 25 | return holder 26 | } 27 | 28 | 29 | override fun onBindViewHolder(holder: MultiSelectViewHolder, position: Int) { 30 | val item = getItem(position) 31 | holder.bind(item, showFileSize) 32 | holder.itemView.tag = item 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/adapters/multi/MultiSelectViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.adapters.multi 2 | 3 | import androidx.core.view.isVisible 4 | import com.crazylegend.core.adapters.BaseViewHolder 5 | import com.crazylegend.core.bytesToFormattedString 6 | import com.crazylegend.core.databinding.ItemviewImageBinding 7 | import com.crazylegend.core.dto.BaseCursorModel 8 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 9 | import com.crazylegend.core.visible 10 | 11 | 12 | /** 13 | * Created by crazy on 5/8/20 to long live and prosper ! 14 | */ 15 | class MultiSelectViewHolder( 16 | private val binding: ItemviewImageBinding, 17 | private val modifier: BaseMultiPickerModifier? 18 | ) : BaseViewHolder(binding) { 19 | 20 | private val selectIconModifier get() = modifier?.selectIconModifier 21 | private val unSelectedIconModifier get() = modifier?.unSelectedIconModifier 22 | private val viewHolderPlaceholderModifier get() = modifier?.viewHolderPlaceholderModifier 23 | 24 | init { 25 | binding.selection.visible() 26 | modifier?.applyGravity(binding.selection) 27 | } 28 | 29 | fun bind(cursorModel: BaseCursorModel, showFileSize: Boolean) { 30 | loadImage(binding.image, cursorModel.contentUri, viewHolderPlaceholderModifier) 31 | if (showFileSize) { 32 | modifier?.sizeTextModifier?.applyTextParams(binding.size) 33 | modifier?.sizeTextModifier?.applyTextParamsConstraint(binding.size) 34 | binding.size.isVisible = false 35 | cursorModel.size?.let { size -> 36 | binding.size.isVisible = size > 0 37 | binding.size.text = size.bytesToFormattedString() 38 | } 39 | } 40 | if (cursorModel.isSelected) { 41 | setupSelectedImage(binding.selection, selectIconModifier) 42 | } else { 43 | setupUnselectedImage(binding.selection, unSelectedIconModifier) 44 | } 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/adapters/single/SingleAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.adapters.single 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.ListAdapter 5 | import com.crazylegend.core.databinding.ItemviewImageBinding 6 | import com.crazylegend.core.dto.BaseCursorModel 7 | import com.crazylegend.core.inflater 8 | import com.crazylegend.core.modifiers.SizeTextModifier 9 | import com.crazylegend.core.modifiers.single.ImageModifier 10 | 11 | 12 | /** 13 | * Created by crazy on 5/8/20 to long live and prosper ! 14 | */ 15 | 16 | open class SingleAdapter(private val showFileSize: Boolean, 17 | private val viewHolderPlaceholderModifier: ImageModifier?, 18 | private val sizeTextModifier: SizeTextModifier?, 19 | private val onClick: (BaseCursorModel) -> Unit) : ListAdapter(SingleDiffUtil()) { 20 | 21 | override fun onCreateViewHolder(parent: ViewGroup, viewBaseCursorModelype: Int) = 22 | SingleViewHolder(ItemviewImageBinding.inflate(parent.inflater, parent, false), viewHolderPlaceholderModifier, sizeTextModifier, onClick) 23 | 24 | override fun onBindViewHolder(holder: SingleViewHolder, position: Int) { 25 | val item = getItem(position) 26 | holder.bind(item, showFileSize) 27 | holder.itemView.tag = item 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/adapters/single/SingleDiffUtil.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.adapters.single 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.crazylegend.core.dto.BaseCursorModel 5 | 6 | 7 | /** 8 | * Created by crazy on 5/11/20 to long live and prosper ! 9 | */ 10 | class SingleDiffUtil : DiffUtil.ItemCallback() { 11 | override fun areItemsTheSame(oldItem: BaseCursorModel, newItem: BaseCursorModel) = 12 | oldItem.id == newItem.id 13 | 14 | override fun areContentsTheSame(oldItem: BaseCursorModel, newItem: BaseCursorModel) = 15 | (oldItem.id == newItem.id) && (oldItem.isSelected == newItem.isSelected) 16 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/adapters/single/SingleViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.adapters.single 2 | 3 | import androidx.core.view.isVisible 4 | import com.crazylegend.core.adapters.BaseViewHolder 5 | import com.crazylegend.core.bytesToFormattedString 6 | import com.crazylegend.core.databinding.ItemviewImageBinding 7 | import com.crazylegend.core.dto.BaseCursorModel 8 | import com.crazylegend.core.gone 9 | import com.crazylegend.core.modifiers.SizeTextModifier 10 | import com.crazylegend.core.modifiers.single.ImageModifier 11 | 12 | 13 | /** 14 | * Created by crazy on 5/8/20 to long live and prosper ! 15 | */ 16 | 17 | class SingleViewHolder(private val binding: ItemviewImageBinding, 18 | private val viewHolderPlaceholderModifier: ImageModifier?, private val sizeTextModifier: SizeTextModifier?, onClick: (BaseCursorModel) -> Unit) : 19 | BaseViewHolder(binding) { 20 | 21 | fun bind(item: BaseCursorModel, showFileSize: Boolean) { 22 | loadImage(binding.image, item.contentUri, viewHolderPlaceholderModifier) 23 | if (showFileSize) { 24 | sizeTextModifier?.applyTextParams(binding.size) 25 | sizeTextModifier?.applyTextParamsConstraint(binding.size) 26 | binding.size.isVisible = false 27 | item.size?.let { size -> 28 | binding.size.isVisible = size > 0 29 | binding.size.text = size.bytesToFormattedString() 30 | } 31 | } 32 | } 33 | 34 | 35 | private val getModel get() = itemView.tag as? BaseCursorModel? 36 | 37 | init { 38 | itemView.setOnClickListener { 39 | val model = getModel ?: return@setOnClickListener 40 | onClick(model) 41 | } 42 | binding.selection.gone() 43 | } 44 | 45 | 46 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/consts/Consts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.consts 2 | 3 | 4 | /** 5 | * Created by crazy on 5/11/20 to long live and prosper ! 6 | */ 7 | 8 | const val MODIFIER_ARGUMENT_CONST = "modifier" 9 | -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/contracts/BaseContractMultiPick.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.contracts 2 | 3 | import androidx.activity.result.ActivityResultLauncher 4 | import androidx.appcompat.widget.AppCompatTextView 5 | import com.crazylegend.core.R 6 | import com.crazylegend.core.adapters.multi.MultiSelectAdapter 7 | import com.crazylegend.core.consts.MODIFIER_ARGUMENT_CONST 8 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutMultiBinding 9 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 10 | import com.google.android.material.button.MaterialButton 11 | 12 | 13 | /** 14 | * Created by crazy on 5/11/20 to long live and prosper ! 15 | */ 16 | interface BaseContractMultiPick { 17 | val multiSelectAdapter: MultiSelectAdapter? 18 | fun addModifier(modifier: BaseMultiPickerModifier) 19 | val modifier: BaseMultiPickerModifier? 20 | val binding: FragmentImagesGalleryLayoutMultiBinding 21 | val askForStoragePermission: ActivityResultLauncher 22 | val layout get() = R.layout.fragment_images_gallery_layout_multi 23 | fun applyTitleModifications(appCompatTextView: AppCompatTextView) 24 | fun applyDoneButtonModifications(doneButton: MaterialButton) 25 | val modifierTag get() = MODIFIER_ARGUMENT_CONST 26 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/contracts/BaseContractSinglePick.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.contracts 2 | 3 | import androidx.activity.result.ActivityResultLauncher 4 | import androidx.appcompat.widget.AppCompatTextView 5 | import com.crazylegend.core.R 6 | import com.crazylegend.core.adapters.single.SingleAdapter 7 | import com.crazylegend.core.consts.MODIFIER_ARGUMENT_CONST 8 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutBinding 9 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 10 | 11 | 12 | /** 13 | * Created by crazy on 5/11/20 to long live and prosper ! 14 | */ 15 | interface BaseContractSinglePick { 16 | fun addModifier(modifier: BaseSinglePickerModifier) 17 | val modifier: BaseSinglePickerModifier? 18 | fun applyTitleModifications(appCompatTextView: AppCompatTextView) 19 | val modifierTag get() = MODIFIER_ARGUMENT_CONST 20 | val binding: FragmentImagesGalleryLayoutBinding 21 | val askForStoragePermission: ActivityResultLauncher 22 | val layout get() = R.layout.fragment_images_gallery_layout 23 | val singleAdapter: SingleAdapter? 24 | 25 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/dto/BaseCursorModel.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.dto 2 | 3 | import android.content.ContentResolver 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Parcelable 9 | import android.provider.MediaStore 10 | import android.util.Size 11 | import kotlinx.parcelize.Parcelize 12 | import java.util.* 13 | import java.util.concurrent.TimeUnit 14 | 15 | 16 | /** 17 | * Created by crazy on 5/11/20 to long live and prosper ! 18 | */ 19 | 20 | @Parcelize 21 | open class BaseCursorModel( 22 | open val id: Long, 23 | open val displayName: String?, 24 | open val dateAdded: Long?, 25 | open val contentUri: Uri, 26 | open val dateModified: Long?, 27 | open val description: String?, 28 | open val size: Int?, 29 | open val width: Int?, 30 | open val height: Int?, 31 | var isSelected: Boolean = false 32 | ) : Parcelable { 33 | 34 | 35 | /** 36 | * Or just use Glide with the contentUri 37 | * @param contentResolver ContentResolver 38 | * @param customSize Size 39 | * @param options Options? 40 | * @return Bitmap? 41 | */ 42 | @Suppress("DEPRECATION") 43 | fun loadThumbnail(contentResolver: ContentResolver, 44 | customSize: Size = Size(size ?: 150, size ?: 150), 45 | legacyKind: Int = MediaStore.Video.Thumbnails.MICRO_KIND, 46 | options: BitmapFactory.Options? = null): Bitmap? { 47 | 48 | return tryOrNull { 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 50 | contentResolver.loadThumbnail(contentUri, customSize, null) 51 | } else { 52 | MediaStore.Video.Thumbnails.getThumbnail(contentResolver, id, legacyKind, 53 | options ?: BitmapFactory.Options()) 54 | } 55 | } 56 | } 57 | 58 | private fun tryOrNull(function: () -> Bitmap): Bitmap? { 59 | return try { 60 | function() 61 | } catch (e: Exception) { 62 | null 63 | } 64 | } 65 | 66 | val extension get() = displayName?.substringAfterLast(".") 67 | 68 | val addedDateAsDate 69 | get() = dateAdded?.let { 70 | Date(TimeUnit.SECONDS.toMillis(it)) 71 | } 72 | val dateModifiedAsDate 73 | get() = dateModified?.let { 74 | Date(TimeUnit.SECONDS.toMillis(it)) 75 | } 76 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/dto/PickerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.dto 2 | 3 | data class PickerConfig(val showFileSize: Boolean = false) -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/modifiers/SizeTextModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.modifiers 2 | 3 | import android.content.res.ColorStateList 4 | import android.graphics.Color 5 | import android.graphics.Paint 6 | import android.graphics.Typeface 7 | import android.os.Parcelable 8 | import android.widget.TextView 9 | import androidx.appcompat.widget.AppCompatTextView 10 | import androidx.constraintlayout.widget.ConstraintLayout 11 | import androidx.core.content.ContextCompat 12 | import androidx.core.view.setMargins 13 | import androidx.core.view.setPadding 14 | import androidx.core.view.updateLayoutParams 15 | import kotlinx.parcelize.Parcelize 16 | 17 | 18 | /** 19 | * Created by crazy on 5/11/20 to long live and prosper ! 20 | */ 21 | 22 | 23 | @Parcelize 24 | data class SizeTextModifier( 25 | var textColor: Int? = null, 26 | var textPadding: Int? = null, 27 | var textSize: Float? = null, 28 | var startMargin: Int? = null, 29 | var endMargin: Int? = null, 30 | var marginTop: Int? = null, 31 | var marginBottom: Int? = null, 32 | var margin: Int? = null, 33 | var backgroundColor: Int? = null, 34 | var backgroundDrawable: Int? = null, 35 | var textStyle: TextStyle = TextStyle.NORMAL, 36 | var textAlignment: Int = TextView.TEXT_ALIGNMENT_VIEW_START 37 | ) : Parcelable { 38 | 39 | 40 | private val allSizeMarginCondition get() = margin != null 41 | 42 | enum class TextStyle { 43 | BOLD, UNDERLINED, ITALIC, BOLD_ITALIC, NORMAL 44 | } 45 | 46 | private fun updateMargins(textView: AppCompatTextView) { 47 | textView.updateLayoutParams { 48 | startMargin?.let { marginStart = it } 49 | endMargin?.let { marginEnd = it } 50 | marginTop?.let { topMargin = it } 51 | marginBottom?.let { bottomMargin = it } 52 | } 53 | } 54 | 55 | private fun updateMarginsConstraint(textView: AppCompatTextView) { 56 | textView.updateLayoutParams { 57 | startMargin?.let { marginStart = it } 58 | endMargin?.let { marginEnd = it } 59 | marginTop?.let { topMargin = it } 60 | marginBottom?.let { bottomMargin = it } 61 | } 62 | } 63 | 64 | private fun updateAllMargins(textView: AppCompatTextView) { 65 | textView.updateLayoutParams { 66 | margin?.let { setMargins(it) } 67 | } 68 | } 69 | 70 | private fun updateAllMarginsConstraint(textView: AppCompatTextView) { 71 | textView.updateLayoutParams { 72 | margin?.let { setMargins(it) } 73 | } 74 | } 75 | 76 | fun applyTextParams(text: AppCompatTextView) { 77 | textSize?.let { text.textSize = it } 78 | textPadding?.let { text.setPadding(it) } 79 | textColor?.let { text.setTextColor(it) } 80 | text.textAlignment = textAlignment 81 | backgroundColor?.let { text.setBackgroundColor(it) } 82 | applyTextStyle(text) 83 | if (allSizeMarginCondition) { 84 | updateAllMargins(text) 85 | } else { 86 | updateMargins(text) 87 | } 88 | } 89 | 90 | fun applyTextParamsConstraint(text: AppCompatTextView) { 91 | textSize?.let { text.textSize = it } 92 | textPadding?.let { text.setPadding(it) } 93 | textColor?.let { text.setTextColor(it) } 94 | text.textAlignment = textAlignment 95 | backgroundDrawable?.let { text.background = ContextCompat.getDrawable(text.context, it) } 96 | ?: run { 97 | text.backgroundTintList = ColorStateList.valueOf(Color.BLACK) 98 | } 99 | applyTextStyle(text) 100 | if (allSizeMarginCondition) { 101 | updateAllMarginsConstraint(text) 102 | } else { 103 | updateMarginsConstraint(text) 104 | } 105 | } 106 | 107 | private fun applyTextStyle(text: AppCompatTextView) { 108 | when (textStyle) { 109 | TextStyle.BOLD -> { 110 | text.setTypeface(text.typeface, Typeface.BOLD) 111 | } 112 | 113 | TextStyle.UNDERLINED -> { 114 | text.paintFlags = text.paintFlags or Paint.UNDERLINE_TEXT_FLAG 115 | } 116 | 117 | TextStyle.ITALIC -> { 118 | text.setTypeface(text.typeface, Typeface.ITALIC) 119 | } 120 | 121 | TextStyle.BOLD_ITALIC -> { 122 | text.setTypeface(text.typeface, Typeface.BOLD_ITALIC) 123 | } 124 | 125 | else -> { 126 | 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/modifiers/TitleTextModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.modifiers 2 | 3 | import android.graphics.Paint 4 | import android.graphics.Typeface 5 | import android.os.Parcelable 6 | import android.widget.RelativeLayout 7 | import android.widget.TextView 8 | import androidx.appcompat.widget.AppCompatTextView 9 | import androidx.constraintlayout.widget.ConstraintLayout 10 | import androidx.core.view.setMargins 11 | import androidx.core.view.setPadding 12 | import androidx.core.view.updateLayoutParams 13 | import kotlinx.parcelize.Parcelize 14 | 15 | 16 | /** 17 | * Created by crazy on 5/11/20 to long live and prosper ! 18 | */ 19 | 20 | 21 | @Parcelize 22 | data class TitleTextModifier( 23 | var textColor: Int? = null, 24 | var textPadding: Int? = null, 25 | var textSize: Float? = null, 26 | var textString: String? = null, 27 | var startMargin: Int? = null, 28 | var endMargin: Int? = null, 29 | var marginTop: Int? = null, 30 | var marginBottom: Int? = null, 31 | var margin: Int? = null, 32 | var textStyle: TextStyle = TextStyle.NORMAL, 33 | var textAlignment: Int = TextView.TEXT_ALIGNMENT_VIEW_START 34 | ) : Parcelable { 35 | 36 | 37 | private val allSizeMarginCondition get() = margin != null 38 | 39 | enum class TextStyle { 40 | BOLD, UNDERLINED, ITALIC, BOLD_ITALIC, NORMAL 41 | } 42 | 43 | private fun updateMargins(textView: AppCompatTextView) { 44 | textView.updateLayoutParams { 45 | startMargin?.let { marginStart = it } 46 | endMargin?.let { marginEnd = it } 47 | marginTop?.let { topMargin = it } 48 | marginBottom?.let { bottomMargin = it } 49 | } 50 | } 51 | 52 | private fun updateMarginsConstraint(textView: AppCompatTextView) { 53 | textView.updateLayoutParams { 54 | startMargin?.let { marginStart = it } 55 | endMargin?.let { marginEnd = it } 56 | marginTop?.let { topMargin = it } 57 | marginBottom?.let { bottomMargin = it } 58 | } 59 | } 60 | 61 | private fun updateAllMargins(textView: AppCompatTextView) { 62 | textView.updateLayoutParams { 63 | margin?.let { setMargins(it) } 64 | } 65 | } 66 | 67 | private fun updateAllMarginsConstraint(textView: AppCompatTextView) { 68 | textView.updateLayoutParams { 69 | margin?.let { setMargins(it) } 70 | } 71 | } 72 | 73 | fun applyTextParams(text: AppCompatTextView) { 74 | textSize?.let { text.textSize = it } 75 | textPadding?.let { text.setPadding(it) } 76 | textColor?.let { text.setTextColor(it) } 77 | text.textAlignment = textAlignment 78 | applyTextStyle(text) 79 | textString?.let { text.text = it } 80 | if (allSizeMarginCondition) { 81 | updateAllMargins(text) 82 | } else { 83 | updateMargins(text) 84 | } 85 | } 86 | 87 | fun applyTextParamsConstraint(text: AppCompatTextView) { 88 | textSize?.let { text.textSize = it } 89 | textPadding?.let { text.setPadding(it) } 90 | textColor?.let { text.setTextColor(it) } 91 | text.textAlignment = textAlignment 92 | applyTextStyle(text) 93 | textString?.let { text.text = it } 94 | if (allSizeMarginCondition) { 95 | updateAllMarginsConstraint(text) 96 | } else { 97 | updateMarginsConstraint(text) 98 | } 99 | } 100 | 101 | 102 | private fun applyTextStyle(text: AppCompatTextView) { 103 | when (textStyle) { 104 | TextStyle.BOLD -> { 105 | text.setTypeface(text.typeface, Typeface.BOLD) 106 | } 107 | TextStyle.UNDERLINED -> { 108 | text.paintFlags = text.paintFlags or Paint.UNDERLINE_TEXT_FLAG 109 | } 110 | TextStyle.ITALIC -> { 111 | text.setTypeface(text.typeface, Typeface.ITALIC) 112 | } 113 | TextStyle.BOLD_ITALIC -> { 114 | text.setTypeface(text.typeface, Typeface.BOLD_ITALIC) 115 | } 116 | else -> { 117 | 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/modifiers/base/BaseMultiPickerModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.modifiers.base 2 | 3 | import android.os.Parcelable 4 | import android.view.View 5 | import androidx.appcompat.widget.AppCompatImageView 6 | import androidx.core.view.marginBottom 7 | import androidx.core.view.marginLeft 8 | import androidx.core.view.marginRight 9 | import androidx.core.view.marginTop 10 | import com.crazylegend.core.bottom 11 | import com.crazylegend.core.constrainBottomToTopOf 12 | import com.crazylegend.core.left 13 | import com.crazylegend.core.modifiers.SizeTextModifier 14 | import com.crazylegend.core.modifiers.TitleTextModifier 15 | import com.crazylegend.core.modifiers.multi.DoneButtonModifier 16 | import com.crazylegend.core.modifiers.multi.SelectIconModifier 17 | import com.crazylegend.core.modifiers.single.ImageModifier 18 | import com.crazylegend.core.right 19 | import com.crazylegend.core.top 20 | import kotlinx.parcelize.Parcelize 21 | 22 | 23 | /** 24 | * Created by crazy on 5/16/20 to long live and prosper ! 25 | */ 26 | 27 | @Parcelize 28 | open class BaseMultiPickerModifier( 29 | open val doneButtonModifier: DoneButtonModifier = DoneButtonModifier(), 30 | open val titleTextModifier: TitleTextModifier = TitleTextModifier(), 31 | open val selectIconModifier: SelectIconModifier = SelectIconModifier(), 32 | open val unSelectedIconModifier: SelectIconModifier = SelectIconModifier(), 33 | open var indicatorsGravity: Gravity = Gravity.BOTTOM_RIGHT, 34 | open val viewHolderPlaceholderModifier: ImageModifier = ImageModifier(), 35 | open val noContentTextModifier: TitleTextModifier = TitleTextModifier(), 36 | open var loadingIndicatorTint: Int? = null, 37 | open val sizeTextModifier: SizeTextModifier = SizeTextModifier(), 38 | ) : Parcelable { 39 | 40 | enum class Gravity { 41 | TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT 42 | } 43 | 44 | inline fun setupBaseMultiPicker( 45 | doneButtonModifications: DoneButtonModifier.() -> Unit = {}, 46 | titleModifications: TitleTextModifier.() -> Unit = {}, 47 | selectIconModifications: SelectIconModifier.() -> Unit = {}, 48 | unSelectIconModifications: SelectIconModifier.() -> Unit = {}, 49 | viewHolderPlaceholderModifications: ImageModifier.() -> Unit = {}, 50 | gravityForSelectAndUnSelectIndicators: Gravity = Gravity.BOTTOM_RIGHT, 51 | tintForLoadingProgressBar: Int? = null, 52 | sizeTextModifications: SizeTextModifier.() -> Unit = {}, 53 | ) { 54 | doneButtonModifier.doneButtonModifications() 55 | titleTextModifier.titleModifications() 56 | selectIconModifier.selectIconModifications() 57 | unSelectedIconModifier.unSelectIconModifications() 58 | viewHolderPlaceholderModifier.viewHolderPlaceholderModifications() 59 | indicatorsGravity = gravityForSelectAndUnSelectIndicators 60 | loadingIndicatorTint = tintForLoadingProgressBar 61 | sizeTextModifier.sizeTextModifications() 62 | } 63 | 64 | fun applyGravity(imageView: AppCompatImageView) { 65 | when (indicatorsGravity) { 66 | Gravity.TOP_LEFT -> { 67 | imageView.top(imageView.marginTop) 68 | imageView.left(imageView.marginLeft) 69 | } 70 | 71 | Gravity.TOP_RIGHT -> { 72 | imageView.top(imageView.marginTop) 73 | imageView.right(imageView.marginRight) 74 | } 75 | 76 | Gravity.BOTTOM_LEFT -> { 77 | imageView.bottom(imageView.marginBottom) 78 | imageView.left(imageView.marginLeft) 79 | } 80 | 81 | Gravity.BOTTOM_RIGHT -> { 82 | imageView.bottom(imageView.marginBottom) 83 | imageView.right(imageView.marginRight) 84 | } 85 | } 86 | } 87 | 88 | fun applyGravityWithBottomConstraint(imageView: AppCompatImageView, view: View) { 89 | when (indicatorsGravity) { 90 | Gravity.TOP_LEFT -> { 91 | imageView.top(imageView.marginTop) 92 | imageView.left(imageView.marginLeft) 93 | } 94 | 95 | Gravity.TOP_RIGHT -> { 96 | imageView.top(imageView.marginTop) 97 | imageView.right(imageView.marginRight) 98 | } 99 | 100 | Gravity.BOTTOM_LEFT -> { 101 | imageView.constrainBottomToTopOf(view, imageView.marginBottom) 102 | imageView.left(imageView.marginLeft) 103 | } 104 | 105 | Gravity.BOTTOM_RIGHT -> { 106 | imageView.constrainBottomToTopOf(view, imageView.marginBottom) 107 | imageView.right(imageView.marginRight) 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/modifiers/base/BaseSinglePickerModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.modifiers.base 2 | 3 | import android.os.Parcelable 4 | import com.crazylegend.core.modifiers.SizeTextModifier 5 | import com.crazylegend.core.modifiers.TitleTextModifier 6 | import com.crazylegend.core.modifiers.single.ImageModifier 7 | import kotlinx.parcelize.Parcelize 8 | 9 | 10 | /** 11 | * Created by crazy on 5/16/20 to long live and prosper ! 12 | */ 13 | 14 | @Parcelize 15 | open class BaseSinglePickerModifier( 16 | open val titleTextModifier: TitleTextModifier = TitleTextModifier(), 17 | open val viewHolderPlaceholderModifier: ImageModifier = ImageModifier(), 18 | open val noContentTextModifier: TitleTextModifier = TitleTextModifier(), 19 | open var loadingIndicatorTint: Int? = null, 20 | open val sizeTextModifier: SizeTextModifier = SizeTextModifier(), 21 | ) : Parcelable { 22 | 23 | inline fun setupBaseModifier( 24 | loadingIndicatorColor: Int? = null, 25 | titleTextModifications: TitleTextModifier.() -> Unit = {}, 26 | placeHolderModifications: ImageModifier.() -> Unit = {}, 27 | noContentTextModifications: TitleTextModifier.() -> Unit = {}, 28 | sizeTextModifications: SizeTextModifier.() -> Unit = {}, 29 | ) { 30 | titleTextModifier.titleTextModifications() 31 | loadingIndicatorTint = loadingIndicatorColor 32 | viewHolderPlaceholderModifier.placeHolderModifications() 33 | noContentTextModifier.noContentTextModifications() 34 | sizeTextModifier.sizeTextModifications() 35 | } 36 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/modifiers/multi/DoneButtonModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.modifiers.multi 2 | 3 | import android.content.res.ColorStateList 4 | import android.os.Parcelable 5 | import android.widget.RelativeLayout 6 | import androidx.core.view.setMargins 7 | import androidx.core.view.setPadding 8 | import androidx.core.view.updateLayoutParams 9 | import com.google.android.material.button.MaterialButton 10 | import kotlinx.parcelize.Parcelize 11 | 12 | 13 | /** 14 | * Created by crazy on 5/11/20 to long live and prosper ! 15 | */ 16 | 17 | @Parcelize 18 | data class DoneButtonModifier( 19 | var padding: Int? = null, 20 | var startMargin: Int? = null, 21 | var endMargin: Int? = null, 22 | var marginTop: Int? = null, 23 | var marginBottom: Int? = null, 24 | var margin: Int? = null, 25 | var tint: Int? = null, 26 | var cornerRadius: Int? = null, 27 | var rippleColor: Int? = null, 28 | var icon: Int? = null, 29 | var iconGravity: Int? = null, 30 | var iconTint: Int? = null 31 | ) : Parcelable { 32 | 33 | private val allSizeMarginCondition get() = margin != null 34 | 35 | fun applyImageParams(button: MaterialButton) { 36 | padding?.let { button.setPadding(it) } 37 | if (allSizeMarginCondition) { 38 | updateAllMargins(button) 39 | } else { 40 | updateMargins(button) 41 | } 42 | iconTint?.let { button.iconTint = ColorStateList.valueOf(it) } 43 | iconGravity?.let { button.iconGravity = it } 44 | icon?.let { button.setIconResource(it) } 45 | rippleColor?.let { button.rippleColor = ColorStateList.valueOf(it) } 46 | cornerRadius?.let { button.cornerRadius = it } 47 | tint?.let { button.backgroundTintList = ColorStateList.valueOf(it) } 48 | } 49 | 50 | private fun updateMargins(button: MaterialButton) { 51 | button.updateLayoutParams { 52 | startMargin?.let { marginStart = it } 53 | endMargin?.let { marginEnd = it } 54 | marginTop?.let { topMargin = it } 55 | marginBottom?.let { bottomMargin = it } 56 | } 57 | } 58 | 59 | private fun updateAllMargins(button: MaterialButton) { 60 | button.updateLayoutParams { 61 | margin?.let { setMargins(it) } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/modifiers/multi/SelectIconModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.modifiers.multi 2 | 3 | import android.content.res.ColorStateList 4 | import android.os.Parcelable 5 | import androidx.appcompat.widget.AppCompatImageView 6 | import androidx.constraintlayout.widget.ConstraintLayout 7 | import androidx.core.view.setMargins 8 | import androidx.core.view.setPadding 9 | import androidx.core.view.updateLayoutParams 10 | import androidx.core.widget.ImageViewCompat 11 | import kotlinx.parcelize.Parcelize 12 | 13 | 14 | /** 15 | * Created by crazy on 5/11/20 to long live and prosper ! 16 | */ 17 | 18 | @Parcelize 19 | data class SelectIconModifier( 20 | var resID: Int? = null, 21 | var padding: Int? = null, 22 | var startMargin: Int? = null, 23 | var endMargin: Int? = null, 24 | var marginTop: Int? = null, 25 | var marginBottom: Int? = null, 26 | var margin: Int? = null, 27 | var tint: Int? = null 28 | ) : Parcelable { 29 | 30 | private val allSizeMarginCondition get() = margin != null 31 | 32 | 33 | fun applyImageParams(imageView: AppCompatImageView, resourceID: Int) { 34 | imageView.setImageResource(resourceID) 35 | padding?.let { imageView.setPadding(it) } 36 | tint?.let { ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(it)) } 37 | 38 | if (allSizeMarginCondition) { 39 | updateAllMargins(imageView) 40 | } else { 41 | updateMargins(imageView) 42 | } 43 | } 44 | 45 | private fun updateMargins(imageView: AppCompatImageView) { 46 | imageView.updateLayoutParams { 47 | startMargin?.let { marginStart = it } 48 | endMargin?.let { marginEnd = it } 49 | marginTop?.let { topMargin = it } 50 | marginBottom?.let { bottomMargin = it } 51 | } 52 | } 53 | 54 | private fun updateAllMargins(imageView: AppCompatImageView) { 55 | imageView.updateLayoutParams { 56 | margin?.let { setMargins(it) } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/modifiers/single/ImageModifier.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.modifiers.single 2 | 3 | import android.os.Parcelable 4 | import androidx.appcompat.widget.AppCompatImageView 5 | import androidx.constraintlayout.widget.ConstraintLayout 6 | import androidx.core.view.setMargins 7 | import androidx.core.view.setPadding 8 | import androidx.core.view.updateLayoutParams 9 | import kotlinx.parcelize.Parcelize 10 | 11 | 12 | /** 13 | * Created by crazy on 5/11/20 to long live and prosper ! 14 | */ 15 | 16 | /** 17 | * Only works for dialog fragment since there it's visible 18 | */ 19 | @Parcelize 20 | data class ImageModifier( 21 | var resID: Int? = null, 22 | var padding: Int? = null, 23 | var startMargin: Int? = null, 24 | var endMargin: Int? = null, 25 | var marginTop: Int? = null, 26 | var marginBottom: Int? = null, 27 | var margin: Int? = null 28 | ) : Parcelable { 29 | 30 | private val allSizeMarginCondition get() = margin != null 31 | 32 | fun applyImageParamsConstraintLayout(imageView: AppCompatImageView) { 33 | resID?.let { imageView.setImageResource(it) } 34 | padding?.let { imageView.setPadding(it) } 35 | if (allSizeMarginCondition) { 36 | updateAllMarginsConstraint(imageView) 37 | } else { 38 | updateMarginsConstraint(imageView) 39 | } 40 | } 41 | 42 | 43 | private fun updateMarginsConstraint(imageView: AppCompatImageView) { 44 | imageView.updateLayoutParams { 45 | startMargin?.let { marginStart = it } 46 | endMargin?.let { marginEnd = it } 47 | marginTop?.let { topMargin = it } 48 | marginBottom?.let { bottomMargin = it } 49 | } 50 | } 51 | 52 | private fun updateAllMarginsConstraint(imageView: AppCompatImageView) { 53 | imageView.updateLayoutParams { 54 | margin?.let { setMargins(it) } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /core/src/main/java/com/crazylegend/core/sorting/SortOrder.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core.sorting 2 | 3 | 4 | /** 5 | * Created by crazy on 5/8/20 to long live and prosper ! 6 | */ 7 | enum class SortOrder { 8 | DATE_ADDED_DESC, 9 | DATE_ADDED_ASC, 10 | DISPLAY_NAME_DESC, 11 | DISPLAY_NAME_ASC, 12 | DATE_MODIFIED_DESC, 13 | DATE_MODIFIED_ASC, 14 | SIZE_ASC, 15 | SIZE_DESC 16 | } -------------------------------------------------------------------------------- /core/src/main/res/anim/fragment_fade_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/anim/fragment_fade_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_checked_default.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_image.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_minus.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_unchecked_default.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/rounded_bg_abstract_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /core/src/main/res/layout/fragment_images_gallery_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 33 | 34 | 38 | 39 | 48 | 49 | 50 | 57 | 58 | 64 | 65 | -------------------------------------------------------------------------------- /core/src/main/res/layout/fragment_images_gallery_layout_multi.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 32 | 33 | 38 | 39 | 51 | 52 | 63 | 64 | 65 | 66 | 73 | 74 | 80 | -------------------------------------------------------------------------------- /core/src/main/res/layout/itemview_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 25 | 26 | 41 | 42 | -------------------------------------------------------------------------------- /core/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #BDBDBD 4 | #FFFFFF 5 | #000000 6 | -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Done 4 | ¯\_(ツ)_/¯\n\nNo content has been found on your device 5 | -------------------------------------------------------------------------------- /core/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /core/src/test/java/com/crazylegend/core/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.core 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extensions/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /extensions/build.gradle: -------------------------------------------------------------------------------- 1 | android { 2 | namespace 'com.crazylegend.extensions' 3 | } -------------------------------------------------------------------------------- /extensions/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/extensions/consumer-rules.pro -------------------------------------------------------------------------------- /extensions/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /extensions/src/androidTest/java/com/crazylegend/extensions/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.extensions 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.crazylegend.extensions.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /extensions/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /extensions/src/main/java/com/crazylegend/extensions/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.extensions 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.database.ContentObserver 6 | import android.database.Cursor 7 | import android.net.Uri 8 | import android.os.Handler 9 | import android.os.Looper 10 | import android.view.View 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.fragment.app.Fragment 13 | import androidx.fragment.app.FragmentManager 14 | import androidx.lifecycle.AndroidViewModel 15 | 16 | 17 | /** 18 | * Created by crazy on 5/11/20 to long live and prosper ! 19 | */ 20 | 21 | fun View.visible() { 22 | this.visibility = View.VISIBLE 23 | } 24 | 25 | fun View.gone() { 26 | this.visibility = View.GONE 27 | } 28 | 29 | fun Cursor.getSafeColumn(column: String): Int? { 30 | return try { 31 | getColumnIndexOrThrow(column) 32 | } catch (e: Exception) { 33 | null 34 | } 35 | } 36 | 37 | fun Context.setupManager(): FragmentManager { 38 | val manager = when (this) { 39 | is Fragment -> this.childFragmentManager 40 | is AppCompatActivity -> if (isFinishing) null else this.supportFragmentManager 41 | else -> null 42 | } 43 | requireNotNull(manager) { 44 | "Use a Fragment or AppCompat activity" 45 | } 46 | return manager 47 | } 48 | 49 | 50 | val AndroidViewModel.context: Context 51 | get() = getApplication() 52 | 53 | 54 | inline fun ContentResolver.registerObserver( 55 | uri: Uri, 56 | crossinline observer: (change: Boolean) -> Unit 57 | ): ContentObserver { 58 | val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { 59 | override fun onChange(selfChange: Boolean) { 60 | observer(selfChange) 61 | } 62 | } 63 | registerContentObserver(uri, true, contentObserver) 64 | return contentObserver 65 | } 66 | 67 | fun Int.bytesToMegabytes(): Double { 68 | return this / (1024.0 * 1024.0) 69 | } 70 | -------------------------------------------------------------------------------- /extensions/src/main/java/com/crazylegend/extensions/FragmentViewBindingDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.extensions 2 | 3 | 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.lifecycle.DefaultLifecycleObserver 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleOwner 9 | import androidx.lifecycle.observe 10 | import androidx.viewbinding.ViewBinding 11 | import kotlin.properties.ReadOnlyProperty 12 | import kotlin.reflect.KProperty 13 | 14 | class FragmentViewBindingDelegate(val fragment: Fragment, val viewBinder: (View) -> T) : ReadOnlyProperty { 15 | 16 | private var fragmentBinding: T? = null 17 | 18 | init { 19 | fragment.lifecycle.addObserver(object : DefaultLifecycleObserver { 20 | override fun onCreate(owner: LifecycleOwner) { 21 | fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner -> 22 | viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { 23 | override fun onDestroy(owner: LifecycleOwner) { 24 | fragmentBinding = null 25 | } 26 | }) 27 | } 28 | } 29 | }) 30 | } 31 | 32 | override fun getValue(thisRef: Fragment, property: KProperty<*>): T { 33 | val binding = fragmentBinding 34 | if (binding != null) { 35 | return binding 36 | } 37 | 38 | val lifecycle = fragment.viewLifecycleOwner.lifecycle 39 | if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { 40 | throw IllegalStateException("Fragment views are destroyed.") 41 | } 42 | return viewBinder(thisRef.requireView()).also { fragmentBinding = it } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /extensions/src/main/java/com/crazylegend/extensions/ViewBindingExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.extensions 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.fragment.app.Fragment 7 | import androidx.viewbinding.ViewBinding 8 | 9 | 10 | inline fun AppCompatActivity.viewBinding(crossinline bindingInflater: (LayoutInflater) -> T) = 11 | lazy(LazyThreadSafetyMode.NONE) { 12 | bindingInflater.invoke(layoutInflater) 13 | } 14 | 15 | 16 | fun Fragment.viewBinding(viewBindingFactory: (View) -> T) = 17 | FragmentViewBindingDelegate(this, viewBindingFactory) 18 | 19 | -------------------------------------------------------------------------------- /extensions/src/test/java/com/crazylegend/extensions/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.extensions 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin.code.style=obsolete 5 | kapt.incremental.apt=true 6 | org.gradle.parallel=true 7 | org.gradle.caching=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Dec 02 12:28:58 CET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /imagepicker/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /imagepicker/build.gradle: -------------------------------------------------------------------------------- 1 | android { 2 | namespace 'com.crazylegend.imagepicker' 3 | } -------------------------------------------------------------------------------- /imagepicker/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/imagepicker/consumer-rules.pro -------------------------------------------------------------------------------- /imagepicker/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /imagepicker/screens/screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/imagepicker/screens/screen_1.png -------------------------------------------------------------------------------- /imagepicker/screens/screen_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/imagepicker/screens/screen_3.png -------------------------------------------------------------------------------- /imagepicker/src/androidTest/java/com/crazylegend/imagepicker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.crazylegend.imagepicker.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /imagepicker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/consts/DialogConsts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.consts 2 | 3 | 4 | /** 5 | * Created by crazy on 5/8/20 to long live and prosper ! 6 | */ 7 | 8 | 9 | const val SINGLE_PICKER_BOTTOM_SHEET = "imagesSinglePickerBottomSheetDialog" 10 | const val SINGLE_PICKER_DIALOG = "imagesSinglePickerDialog" 11 | 12 | 13 | const val MULTI_PICKER_BOTTOM_SHEET = "imagesMultiPickerBottomSheetDialog" 14 | const val MULTI_PICKER_DIALOG = "imagesMultiPickerDialog" 15 | 16 | -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/contracts/MultiPickerContracts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.contracts 2 | 3 | import com.crazylegend.core.contracts.BaseContractMultiPick 4 | import com.crazylegend.imagepicker.images.ImagesVM 5 | import com.crazylegend.imagepicker.listeners.onImagesPicked 6 | import com.crazylegend.imagepicker.pickers.MultiImagePicker 7 | 8 | 9 | /** 10 | * Created by crazy on 5/8/20 to long live and prosper ! 11 | */ 12 | internal interface MultiPickerContracts : BaseContractMultiPick { 13 | val imagesVM: ImagesVM 14 | var onImagesPicked: onImagesPicked? 15 | val errorTag: String get() = MultiImagePicker::javaClass.name 16 | 17 | } -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/contracts/SinglePickerContracts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.contracts 2 | 3 | import com.crazylegend.core.contracts.BaseContractSinglePick 4 | import com.crazylegend.imagepicker.images.ImagesVM 5 | import com.crazylegend.imagepicker.listeners.onImagePicked 6 | import com.crazylegend.imagepicker.pickers.SingleImagePicker 7 | 8 | 9 | /** 10 | * Created by crazy on 5/8/20 to long live and prosper ! 11 | */ 12 | internal interface SinglePickerContracts : BaseContractSinglePick { 13 | val imagesVM: ImagesVM 14 | var onImagePicked: onImagePicked? 15 | val errorTag get() = SingleImagePicker::javaClass.name 16 | } -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/dialogs/multi/MultiImagePickerBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.dialogs.multi 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import androidx.core.os.bundleOf 12 | import androidx.fragment.app.setFragmentResult 13 | import androidx.fragment.app.viewModels 14 | import androidx.lifecycle.observe 15 | import com.crazylegend.core.abstracts.AbstractBottomSheetDialogFragment 16 | import com.crazylegend.core.adapters.multi.MultiSelectAdapter 17 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutMultiBinding 18 | import com.crazylegend.core.dto.PickerConfig 19 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 20 | 21 | import com.crazylegend.extensions.viewBinding 22 | import com.crazylegend.imagepicker.contracts.MultiPickerContracts 23 | import com.crazylegend.imagepicker.images.ImageModel 24 | import com.crazylegend.imagepicker.images.ImagesVM 25 | import com.crazylegend.imagepicker.listeners.onImagesPicked 26 | import com.crazylegend.imagepicker.pickers.MultiImagePicker 27 | import com.google.android.material.button.MaterialButton 28 | 29 | 30 | /** 31 | * Created by crazy on 5/8/20 to long live and prosper ! 32 | */ 33 | internal class MultiImagePickerBottomSheetDialog : AbstractBottomSheetDialogFragment(), 34 | MultiPickerContracts { 35 | 36 | var extensions: Array? = arrayOf() 37 | var pickerConfig: PickerConfig = PickerConfig() 38 | 39 | override val layout: Int 40 | get() = super.layout 41 | override var onImagesPicked: onImagesPicked? = null 42 | override val binding by viewBinding(FragmentImagesGalleryLayoutMultiBinding::bind) 43 | override val imagesVM by viewModels() 44 | override val modifier: BaseMultiPickerModifier? 45 | get() = arguments?.getParcelable(modifierTag) 46 | 47 | override val multiSelectAdapter by lazy { 48 | MultiSelectAdapter(modifier, pickerConfig.showFileSize) 49 | } 50 | override val askForStoragePermission = 51 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { 52 | if (it) { 53 | imagesVM.loadImages(extensions = extensions) 54 | } else { 55 | Log.e(errorTag, "PERMISSION DENIED") 56 | dismissAllowingStateLoss() 57 | } 58 | } 59 | 60 | @Suppress("UNCHECKED_CAST") 61 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 62 | super.onViewCreated(view, savedInstanceState) 63 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 64 | askForStoragePermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE) 65 | } else { 66 | askForStoragePermission.launch(Manifest.permission.READ_MEDIA_IMAGES) 67 | } 68 | setupUIForMultiPicker( 69 | binding.gallery, 70 | multiSelectAdapter, 71 | binding.doneButton, 72 | binding.title, 73 | binding.loadingIndicator, 74 | modifier?.loadingIndicatorTint, 75 | ::applyDoneButtonModifications, 76 | ::applyTitleModifications 77 | ) 78 | 79 | imagesVM.images.observe(viewLifecycleOwner) { 80 | setupList( 81 | multiSelectAdapter, 82 | it, 83 | binding.noContentText, 84 | modifier?.noContentTextModifier 85 | ) 86 | } 87 | handleUIIndicator(imagesVM.loadingIndicator, binding.loadingIndicator) 88 | 89 | 90 | binding.doneButton.setOnClickListener { 91 | val imagesList = 92 | multiSelectAdapter.currentList.filter { it.isSelected } as? List 93 | ?: emptyList() 94 | setFragmentResult( 95 | MultiImagePicker.MULTI_IMAGE_REQUEST_KEY, 96 | bundleOf(MultiImagePicker.ON_MULTI_IMAGE_PICK_KEY to imagesList) 97 | ) 98 | onImagesPicked?.onImagesPicked(imagesList) 99 | dismissAllowingStateLoss() 100 | } 101 | } 102 | 103 | override fun onDestroyView() { 104 | super.onDestroyView() 105 | onImagesPicked = null 106 | } 107 | 108 | override fun addModifier(modifier: BaseMultiPickerModifier) { 109 | arguments = bundleOf(modifierTag to modifier) 110 | } 111 | 112 | override fun applyTitleModifications(appCompatTextView: AppCompatTextView) { 113 | modifier?.titleTextModifier?.applyTextParams(appCompatTextView) 114 | } 115 | 116 | override fun applyDoneButtonModifications(doneButton: MaterialButton) { 117 | modifier?.doneButtonModifier?.applyImageParams(doneButton) 118 | } 119 | } -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/dialogs/single/SingleImagePickerBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.dialogs.single 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import androidx.core.os.bundleOf 12 | import androidx.fragment.app.setFragmentResult 13 | import androidx.fragment.app.viewModels 14 | import androidx.lifecycle.observe 15 | import com.crazylegend.core.abstracts.AbstractBottomSheetDialogFragment 16 | import com.crazylegend.core.adapters.single.SingleAdapter 17 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutBinding 18 | import com.crazylegend.core.dto.PickerConfig 19 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 20 | import com.crazylegend.extensions.viewBinding 21 | import com.crazylegend.imagepicker.contracts.SinglePickerContracts 22 | import com.crazylegend.imagepicker.images.ImageModel 23 | import com.crazylegend.imagepicker.images.ImagesVM 24 | import com.crazylegend.imagepicker.listeners.onImagePicked 25 | import com.crazylegend.imagepicker.pickers.SingleImagePicker 26 | 27 | 28 | /** 29 | * Created by crazy on 5/8/20 to long live and prosper ! 30 | */ 31 | internal class SingleImagePickerBottomSheetDialog : AbstractBottomSheetDialogFragment(), 32 | SinglePickerContracts { 33 | 34 | override val layout: Int 35 | get() = super.layout 36 | override var onImagePicked: onImagePicked? = null 37 | override val binding by viewBinding(FragmentImagesGalleryLayoutBinding::bind) 38 | override val imagesVM by viewModels() 39 | override val modifier: BaseSinglePickerModifier? 40 | get() = arguments?.getParcelable(modifierTag) 41 | var extensions: Array? = arrayOf() 42 | var pickerConfig: PickerConfig = PickerConfig() 43 | 44 | override val singleAdapter by lazy { 45 | SingleAdapter(pickerConfig.showFileSize, modifier?.viewHolderPlaceholderModifier, modifier?.sizeTextModifier) { 46 | val image = it as ImageModel 47 | setFragmentResult( 48 | SingleImagePicker.SINGLE_IMAGE_REQUEST_KEY, 49 | bundleOf(SingleImagePicker.ON_SINGLE_IMAGE_PICK_KEY to image) 50 | ) 51 | onImagePicked?.forImage(image) 52 | dismissAllowingStateLoss() 53 | } 54 | } 55 | 56 | override val askForStoragePermission = 57 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { 58 | if (it) { 59 | imagesVM.loadImages(extensions = extensions) 60 | } else { 61 | Log.e(errorTag, "PERMISSION DENIED") 62 | dismissAllowingStateLoss() 63 | } 64 | } 65 | 66 | 67 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 68 | super.onViewCreated(view, savedInstanceState) 69 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 70 | askForStoragePermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE) 71 | } else { 72 | askForStoragePermission.launch(Manifest.permission.READ_MEDIA_IMAGES) 73 | } 74 | setupUIForSinglePicker( 75 | binding.gallery, 76 | singleAdapter, 77 | binding.title, 78 | binding.loadingIndicator, 79 | modifier?.loadingIndicatorTint, 80 | ::applyTitleModifications 81 | ) 82 | imagesVM.images.observe(viewLifecycleOwner) { 83 | setupList(singleAdapter, it, binding.noContentText, modifier?.noContentTextModifier) 84 | } 85 | handleUIIndicator(imagesVM.loadingIndicator, binding.loadingIndicator) 86 | 87 | } 88 | 89 | override fun onDestroyView() { 90 | super.onDestroyView() 91 | onImagePicked = null 92 | } 93 | 94 | override fun applyTitleModifications(appCompatTextView: AppCompatTextView) { 95 | modifier?.titleTextModifier?.applyTextParams(appCompatTextView) 96 | } 97 | 98 | override fun addModifier(modifier: BaseSinglePickerModifier) { 99 | arguments = bundleOf(modifierTag to modifier) 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/images/ImageModel.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.images 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import com.crazylegend.core.dto.BaseCursorModel 6 | import kotlinx.parcelize.Parcelize 7 | 8 | /** 9 | * Simple data class to hold information about an image included in the device's MediaStore. 10 | */ 11 | 12 | @Parcelize 13 | data class ImageModel(override val id: Long, 14 | override val displayName: String?, 15 | override val dateAdded: Long?, 16 | override val contentUri: Uri, 17 | override val dateModified: Long?, 18 | override val description: String?, 19 | override val size: Int?, 20 | override val width: Int?, 21 | override val height: Int?) : 22 | BaseCursorModel(id, displayName, dateAdded, contentUri, dateModified, description, size, width, height), Parcelable -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/listeners/onImagePicked.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.listeners 2 | 3 | import com.crazylegend.imagepicker.images.ImageModel 4 | 5 | 6 | /** 7 | * Created by crazy on 5/8/20 to long live and prosper ! 8 | */ 9 | internal fun interface onImagePicked { 10 | fun forImage(imageModel: ImageModel) 11 | } 12 | 13 | internal inline fun onImageDSL(crossinline imageCallback: (image: ImageModel) -> Unit) = onImagePicked { imageModel -> imageCallback(imageModel) } -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/listeners/onImagesPicked.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.listeners 2 | 3 | import com.crazylegend.imagepicker.images.ImageModel 4 | 5 | 6 | /** 7 | * Created by crazy on 5/8/20 to long live and prosper ! 8 | */ 9 | internal interface onImagesPicked { 10 | fun onImagesPicked(images: List = emptyList()) 11 | } 12 | 13 | internal inline fun onImagesDSL(crossinline callback: (list: List) -> Unit) = object : onImagesPicked { 14 | override fun onImagesPicked(images: List) { 15 | callback(images) 16 | } 17 | } -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/pickers/MultiImagePicker.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.pickers 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.fragment.app.FragmentManager 6 | import com.crazylegend.core.dto.PickerConfig 7 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 8 | import com.crazylegend.core.setupModifier 9 | import com.crazylegend.extensions.setupManager 10 | import com.crazylegend.imagepicker.consts.MULTI_PICKER_BOTTOM_SHEET 11 | import com.crazylegend.imagepicker.consts.MULTI_PICKER_DIALOG 12 | import com.crazylegend.imagepicker.dialogs.multi.MultiImagePickerBottomSheetDialog 13 | import com.crazylegend.imagepicker.images.ImageModel 14 | import com.crazylegend.imagepicker.listeners.onImagesDSL 15 | 16 | 17 | /** 18 | * Created by crazy on 5/8/20 to long live and prosper ! 19 | */ 20 | object MultiImagePicker { 21 | 22 | 23 | const val MULTI_IMAGE_REQUEST_KEY = "multiImagesRequest" 24 | const val ON_MULTI_IMAGE_PICK_KEY = "onMultiImagesPicked" 25 | 26 | fun restoreListener(context: Context, imagesList: (list: List) -> Unit = {}) { 27 | val manager = context.setupManager() 28 | when (val fragment = manager.findFragmentByTag(MULTI_PICKER_BOTTOM_SHEET) 29 | ?: manager.findFragmentByTag(MULTI_PICKER_DIALOG)) { 30 | is MultiImagePickerBottomSheetDialog -> { 31 | fragment.onImagesPicked = onImagesDSL(imagesList) 32 | } 33 | 34 | null -> { 35 | Log.e(MultiImagePicker::class.java.name, "FRAGMENT NOT FOUND") 36 | } 37 | } 38 | } 39 | 40 | 41 | fun showPicker( 42 | context: Context, extensions: Array? = arrayOf(), 43 | pickerConfig: PickerConfig = PickerConfig(), 44 | multiImagePickerModifier: BaseMultiPickerModifier.() -> Unit = {}, 45 | imagesList: (list: List) -> Unit = {} 46 | ) { 47 | val manager = context.setupManager() 48 | val modifier = setupModifier(multiImagePickerModifier) 49 | with(MultiImagePickerBottomSheetDialog()) { 50 | this.extensions = extensions 51 | this.pickerConfig = pickerConfig 52 | addModifier(modifier) 53 | onImagesPicked = onImagesDSL(imagesList) 54 | show(manager, MULTI_PICKER_BOTTOM_SHEET) 55 | } 56 | } 57 | 58 | fun showPicker( 59 | fragmentManager: FragmentManager, 60 | extensions: Array? = arrayOf(), 61 | pickerConfig: PickerConfig = PickerConfig(), 62 | multiImagePickerModifier: BaseMultiPickerModifier.() -> Unit = {}, 63 | imagesList: (list: List) -> Unit = {} 64 | ) { 65 | val modifier = setupModifier(multiImagePickerModifier) 66 | with(MultiImagePickerBottomSheetDialog()) { 67 | this.extensions = extensions 68 | this.pickerConfig = pickerConfig 69 | addModifier(modifier) 70 | onImagesPicked = onImagesDSL(imagesList) 71 | show(fragmentManager, MULTI_PICKER_BOTTOM_SHEET) 72 | } 73 | } 74 | 75 | 76 | } -------------------------------------------------------------------------------- /imagepicker/src/main/java/com/crazylegend/imagepicker/pickers/SingleImagePicker.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker.pickers 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.fragment.app.FragmentManager 6 | import com.crazylegend.core.dto.PickerConfig 7 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 8 | import com.crazylegend.core.setupModifier 9 | import com.crazylegend.extensions.setupManager 10 | import com.crazylegend.imagepicker.consts.SINGLE_PICKER_BOTTOM_SHEET 11 | import com.crazylegend.imagepicker.consts.SINGLE_PICKER_DIALOG 12 | import com.crazylegend.imagepicker.dialogs.single.SingleImagePickerBottomSheetDialog 13 | import com.crazylegend.imagepicker.images.ImageModel 14 | import com.crazylegend.imagepicker.listeners.onImageDSL 15 | 16 | 17 | /** 18 | * Created by crazy on 5/8/20 to long live and prosper ! 19 | */ 20 | object SingleImagePicker { 21 | 22 | const val SINGLE_IMAGE_REQUEST_KEY = "singleImageRequest" 23 | const val ON_SINGLE_IMAGE_PICK_KEY = "onSingleImagePicked" 24 | 25 | fun restoreListener(context: Context, onPickedImage: (image: ImageModel) -> Unit = {}) { 26 | val manager = context.setupManager() 27 | when (val fragment = manager.findFragmentByTag(SINGLE_PICKER_BOTTOM_SHEET) 28 | ?: manager.findFragmentByTag(SINGLE_PICKER_DIALOG)) { 29 | is SingleImagePickerBottomSheetDialog -> { 30 | fragment.onImagePicked = onImageDSL(onPickedImage) 31 | } 32 | 33 | null -> { 34 | Log.e(SingleImagePicker::class.java.name, "FRAGMENT NOT FOUND") 35 | } 36 | } 37 | } 38 | 39 | fun showPicker( 40 | context: Context, 41 | extensions: Array? = arrayOf(), 42 | pickerConfig: PickerConfig = PickerConfig(), 43 | pickerModifier: BaseSinglePickerModifier.() -> Unit = {}, 44 | onPickedImage: (image: ImageModel) -> Unit = {} 45 | ) { 46 | val modifier = setupModifier(pickerModifier) 47 | val manager = context.setupManager() 48 | with(SingleImagePickerBottomSheetDialog()) { 49 | this.extensions = extensions 50 | this.pickerConfig = pickerConfig 51 | addModifier(modifier) 52 | onImagePicked = onImageDSL(onPickedImage) 53 | show(manager, SINGLE_PICKER_BOTTOM_SHEET) 54 | } 55 | } 56 | 57 | fun showPicker( 58 | fragmentManager: FragmentManager, 59 | extensions: Array? = arrayOf(), 60 | pickerConfig: PickerConfig = PickerConfig(), 61 | pickerModifier: BaseSinglePickerModifier.() -> Unit = {}, 62 | onPickedImage: (image: ImageModel) -> Unit = {} 63 | ) { 64 | val modifier = setupModifier(pickerModifier) 65 | with(SingleImagePickerBottomSheetDialog()) { 66 | this.extensions = extensions 67 | this.pickerConfig = pickerConfig 68 | addModifier(modifier) 69 | onImagePicked = onImageDSL(onPickedImage) 70 | show(fragmentManager, SINGLE_PICKER_BOTTOM_SHEET) 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /imagepicker/src/test/java/com/crazylegend/imagepicker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.imagepicker 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | install: 4 | - ./gradlew publishToMavenLocal 5 | - find . -name "*.aar" -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='MediaPicker' 2 | include ':app' 3 | include ':imagepicker' 4 | include ':core' 5 | include ':videopicker' 6 | include ':extensions' 7 | include ':audiopicker' 8 | -------------------------------------------------------------------------------- /videopicker/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /videopicker/README.md: -------------------------------------------------------------------------------- 1 | 2 | Current jitpack version: [![](https://jitpack.io/v/FunkyMuse/MediaPicker.svg)](https://jitpack.io/#CraZyLegenD/MediaPicker) 3 | 4 | 1. You must declare the permission in your manifest 5 | ```xml 6 | 7 | ``` 8 | 2. If you haven't included the dependencies from the previous screen now's a good time 9 | ```gradle 10 | dependencies { 11 | def pickerVersion = "1.0.0" //look-up the latest one on jitpack 12 | 13 | //the core package is a must 14 | implementation "com.github.FunkyMuse.MediaPicker:core:$pickerVersion" 15 | 16 | //videos 17 | implementation "com.github.FunkyMuse.MediaPicker:videopicker:$pickerVersion" 18 | } 19 | ``` 20 | 3. How to use single picker and check out [how to customize single video picker](https://github.com/FunkyMuse/MediaPicker/wiki/Single--image-video-picker-customization) 21 | ```kotlin 22 | import java.awt.Color 23 | 24 | //simple usage without customization 25 | SingleVideoPicker.showPicker(context = this, onPickedVideo = ::loadVideo) 26 | 27 | //customized 28 | SingleVideoPicker.showPicker(this, extensions = arrayOf(),config = Config(showFileSize = true),{ 29 | setupBaseModifier( 30 | loadingIndicatorColor = R.color.minusColor, 31 | titleTextModifications = { 32 | textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START 33 | textStyle = TitleTextModifier.TextStyle.ITALIC 34 | textColor = Color.BLACK 35 | marginBottom = 30 // use dp or sp this is only for demonstration purposes 36 | textPadding = 5 // use dp or sp this is only for demonstration purposes 37 | textSize = 30f // use sp this is only for demonstration purposes 38 | textString = "Pick a video" 39 | }, 40 | placeHolderModifications = { 41 | resID = R.drawable.ic_image 42 | } 43 | ) 44 | }, ::loadVideo) 45 | 46 | // You can filter files by passing extensions like extensions = arrayOf("mp4","avi") 47 | 48 | ``` 49 | 50 | 4. How to use multi picker and check out [how to customize multi video picker](https://github.com/FunkyMuse/MediaPicker/wiki/Multi-image-video-picker-customization) 51 | ```kotlin 52 | //simple usage without customization 53 | MultiVideoPicker.showPicker(this) { loadVideos(it) } 54 | 55 | //customized 56 | MultiVideoPicker.showPicker(this, extensions = arrayOf(),config = Config(showFileSize = false),{ 57 | setupBaseMultiPicker( 58 | tintForLoadingProgressBar = ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryDark), 59 | gravityForSelectAndUnSelectIndicators = BaseMultiPickerModifier.Gravity.TOP_RIGHT, 60 | titleModifications = { 61 | textAlignment = TextView.TEXT_ALIGNMENT_CENTER 62 | textStyle = TitleTextModifier.TextStyle.ITALIC 63 | textColor = Color.BLACK 64 | marginBottom = 30 // use dp or sp this is only for demonstration purposes 65 | textPadding = 5 // use dp or sp this is only for demonstration purposes 66 | textSize = 30f // use sp this is only for demonstration purposes 67 | textString = "Pick multiple videos" 68 | }, 69 | selectIconModifications = { 70 | resID = R.drawable.ic_checked 71 | tint = Color.BLACK 72 | }, 73 | unSelectIconModifications = { 74 | resID = R.drawable.ic_unchecked 75 | tint = Color.BLACK 76 | }, 77 | viewHolderPlaceholderModifications = { 78 | resID = R.drawable.ic_close 79 | }, 80 | sizeTextModifications = { 81 | textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START 82 | textStyle = SizeTextModifier.TextStyle.NORMAL 83 | margin = 22 // use dp or sp this is only for demonstration purposes 84 | textColor = Color.BLACK 85 | textPadding = 5 // use dp or sp this is only for demonstration purposes 86 | textSize = 12f // use sp this is only for demonstration purposes 87 | backgroundDrawable = R.drawable.rounded_bg_abstract_dialog 88 | } 89 | ) 90 | }, ::doSomethingWithVideoList) 91 | 92 | ``` 93 | 94 | If you're using Fragments to call the pickers you can leverage [set fragment result listener](https://developer.android.com/reference/androidx/fragment/app/FragmentManager#setfragmentresultlistener) to get back the result and you don't have to restore the listener nor provide a lambda for the listener, it can be as simple as 95 | ```kotlin 96 | SingleVideoPicker.showPicker(requireContext()) 97 | ``` 98 | ```kotlin 99 | 100 | setFragmentResultListener(SingleVideoPicker.SINGLE_VIDEO_REQUEST_KEY) { _, bundle -> 101 | val loadedModel = bundle.getParcelable(SingleVideoPicker.ON_SINGLE_VIDEO_PICK_KEY) 102 | loadedModel?.let { loadVideo(it) } 103 | } 104 | 105 | setFragmentResultListener(MultiVideoPicker.MULTI_VIDEO_REQUEST_KEY) { _, bundle -> 106 | val loadedModel = bundle.getParcelableArrayList(MultiVideoPicker.ON_MULTI_VIDEO_PICK_KEY) 107 | loadedModel?.let { doSomethingWithVideoList(it) } 108 | } 109 | ``` 110 | 111 | ## 112 | If you're still not sure how to use, take a look at the [Sample app](https://github.com/FunkyMuse/MediaPicker/blob/master/app/src/main/java/com/crazylegend/mediapicker/MainActivity.kt) 113 | 114 | ## 115 | If you're still not sure how to use fragment listener, take a look at the [Sample app](https://github.com/FunkyMuse/MediaPicker/blob/master/app/src/main/java/com/crazylegend/mediapicker/FragmentResult.kt#L320) 116 | 117 | ## Screens 118 | 119 | Single picker 120 | 121 | 122 | 123 | Multi pickers 124 | 125 | 126 | -------------------------------------------------------------------------------- /videopicker/build.gradle: -------------------------------------------------------------------------------- 1 | android { 2 | namespace 'com.crazylegend.videopicker' 3 | } -------------------------------------------------------------------------------- /videopicker/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/videopicker/consumer-rules.pro -------------------------------------------------------------------------------- /videopicker/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /videopicker/screens/screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/videopicker/screens/screen_1.png -------------------------------------------------------------------------------- /videopicker/screens/screen_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunkyMuse/MediaPicker/3310f617c54475b5f847ae486f7da7621d5e3187/videopicker/screens/screen_3.png -------------------------------------------------------------------------------- /videopicker/src/androidTest/java/com/crazylegend/videopicker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.crazylegend.videopicker.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /videopicker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/consts/DialogConsts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.consts 2 | 3 | 4 | /** 5 | * Created by crazy on 5/9/20 to long live and prosper ! 6 | */ 7 | 8 | const val SINGLE_PICKER_BOTTOM_SHEET = "videoSinglePickerBottomSheetDialog" 9 | const val SINGLE_PICKER_DIALOG = "videoSinglePickerDialog" 10 | 11 | 12 | const val MULTI_PICKER_BOTTOM_SHEET = "videoMultiPickerBottomSheetDialog" 13 | const val MULTI_PICKER_DIALOG = "videoMultiPickerDialog" 14 | 15 | -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/contracts/MultiPickerContracts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.contracts 2 | 3 | import com.crazylegend.core.contracts.BaseContractMultiPick 4 | import com.crazylegend.videopicker.listeners.onVideosPicked 5 | import com.crazylegend.videopicker.pickers.MultiVideoPicker 6 | import com.crazylegend.videopicker.videos.VideosVM 7 | 8 | 9 | /** 10 | * Created by crazy on 5/8/20 to long live and prosper ! 11 | */ 12 | internal interface MultiPickerContracts : BaseContractMultiPick { 13 | val videosVM: VideosVM 14 | var onVideosPicked: onVideosPicked? 15 | val errorTag: String get() = MultiVideoPicker::javaClass.name 16 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/contracts/SinglePickerContracts.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.contracts 2 | 3 | import com.crazylegend.core.contracts.BaseContractSinglePick 4 | import com.crazylegend.videopicker.listeners.onVideoPicked 5 | import com.crazylegend.videopicker.pickers.SingleVideoPicker 6 | import com.crazylegend.videopicker.videos.VideosVM 7 | 8 | 9 | /** 10 | * Created by crazy on 5/8/20 to long live and prosper ! 11 | */ 12 | internal interface SinglePickerContracts : BaseContractSinglePick { 13 | val videosVM: VideosVM 14 | var onVideoPicked: onVideoPicked? 15 | val errorTag get() = SingleVideoPicker::javaClass.name 16 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/dialogs/multi/MultiVideoPickerBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.dialogs.multi 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import androidx.core.os.bundleOf 12 | import androidx.fragment.app.setFragmentResult 13 | import androidx.fragment.app.viewModels 14 | import androidx.lifecycle.observe 15 | import com.crazylegend.core.abstracts.AbstractBottomSheetDialogFragment 16 | import com.crazylegend.core.adapters.multi.MultiSelectAdapter 17 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutMultiBinding 18 | import com.crazylegend.core.dto.PickerConfig 19 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 20 | import com.crazylegend.extensions.viewBinding 21 | import com.crazylegend.videopicker.contracts.MultiPickerContracts 22 | import com.crazylegend.videopicker.listeners.onVideosPicked 23 | import com.crazylegend.videopicker.pickers.MultiVideoPicker 24 | import com.crazylegend.videopicker.videos.VideoModel 25 | import com.crazylegend.videopicker.videos.VideosVM 26 | import com.google.android.material.button.MaterialButton 27 | 28 | 29 | /** 30 | * Created by crazy on 5/8/20 to long live and prosper ! 31 | */ 32 | internal class MultiVideoPickerBottomSheetDialog : AbstractBottomSheetDialogFragment(), 33 | MultiPickerContracts { 34 | 35 | 36 | override val layout: Int 37 | get() = super.layout 38 | override var onVideosPicked: onVideosPicked? = null 39 | override val binding by viewBinding(FragmentImagesGalleryLayoutMultiBinding::bind) 40 | override val videosVM by viewModels() 41 | override val modifier: BaseMultiPickerModifier? 42 | get() = arguments?.getParcelable(modifierTag) 43 | override val multiSelectAdapter by lazy { 44 | MultiSelectAdapter(modifier, pickerConfig.showFileSize) 45 | } 46 | var extensions: Array? = arrayOf() 47 | var pickerConfig: PickerConfig = PickerConfig() 48 | 49 | override val askForStoragePermission = 50 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { 51 | if (it) { 52 | videosVM.loadVideos(extensions = extensions) 53 | } else { 54 | Log.e(errorTag, "PERMISSION DENIED") 55 | dismissAllowingStateLoss() 56 | } 57 | } 58 | 59 | @Suppress("UNCHECKED_CAST") 60 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 61 | super.onViewCreated(view, savedInstanceState) 62 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 63 | askForStoragePermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE) 64 | } else { 65 | askForStoragePermission.launch(Manifest.permission.READ_MEDIA_VIDEO) 66 | } 67 | setupUIForMultiPicker( 68 | binding.gallery, 69 | multiSelectAdapter, 70 | binding.doneButton, 71 | binding.title, 72 | binding.loadingIndicator, 73 | modifier?.loadingIndicatorTint, 74 | ::applyDoneButtonModifications, 75 | ::applyTitleModifications 76 | ) 77 | 78 | videosVM.videos.observe(viewLifecycleOwner) { 79 | setupList( 80 | multiSelectAdapter, 81 | it, 82 | binding.noContentText, 83 | modifier?.noContentTextModifier 84 | ) 85 | } 86 | handleUIIndicator(videosVM.loadingIndicator, binding.loadingIndicator) 87 | 88 | 89 | binding.doneButton.setOnClickListener { 90 | val videoList = 91 | multiSelectAdapter.currentList.filter { it.isSelected } as? List 92 | ?: emptyList() 93 | setFragmentResult( 94 | MultiVideoPicker.MULTI_VIDEO_REQUEST_KEY, 95 | bundleOf(MultiVideoPicker.ON_MULTI_VIDEO_PICK_KEY to videoList) 96 | ) 97 | onVideosPicked?.onVideosPicked(videoList) 98 | dismissAllowingStateLoss() 99 | } 100 | } 101 | 102 | override fun onDestroyView() { 103 | super.onDestroyView() 104 | onVideosPicked = null 105 | } 106 | 107 | override fun addModifier(modifier: BaseMultiPickerModifier) { 108 | arguments = bundleOf(modifierTag to modifier) 109 | } 110 | 111 | override fun applyTitleModifications(appCompatTextView: AppCompatTextView) { 112 | modifier?.titleTextModifier?.applyTextParams(appCompatTextView) 113 | } 114 | 115 | override fun applyDoneButtonModifications(doneButton: MaterialButton) { 116 | modifier?.doneButtonModifier?.applyImageParams(doneButton) 117 | } 118 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/dialogs/single/SingleVideoPickerBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.dialogs.single 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import androidx.core.os.bundleOf 12 | import androidx.fragment.app.setFragmentResult 13 | import androidx.fragment.app.viewModels 14 | import androidx.lifecycle.observe 15 | import com.crazylegend.core.abstracts.AbstractBottomSheetDialogFragment 16 | import com.crazylegend.core.adapters.single.SingleAdapter 17 | import com.crazylegend.core.databinding.FragmentImagesGalleryLayoutBinding 18 | import com.crazylegend.core.dto.PickerConfig 19 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 20 | import com.crazylegend.extensions.viewBinding 21 | import com.crazylegend.videopicker.contracts.SinglePickerContracts 22 | import com.crazylegend.videopicker.listeners.onVideoPicked 23 | import com.crazylegend.videopicker.pickers.SingleVideoPicker 24 | import com.crazylegend.videopicker.videos.VideoModel 25 | import com.crazylegend.videopicker.videos.VideosVM 26 | 27 | 28 | /** 29 | * Created by crazy on 5/8/20 to long live and prosper ! 30 | */ 31 | internal class SingleVideoPickerBottomSheetDialog : AbstractBottomSheetDialogFragment(), 32 | SinglePickerContracts { 33 | 34 | override val layout: Int 35 | get() = super.layout 36 | override var onVideoPicked: onVideoPicked? = null 37 | override val binding by viewBinding(FragmentImagesGalleryLayoutBinding::bind) 38 | override val videosVM by viewModels() 39 | var extensions: Array? = arrayOf() 40 | var pickerConfig: PickerConfig = PickerConfig() 41 | 42 | override val modifier: BaseSinglePickerModifier? get() = arguments?.getParcelable(modifierTag) 43 | 44 | override val singleAdapter by lazy { 45 | SingleAdapter(pickerConfig.showFileSize, modifier?.viewHolderPlaceholderModifier, modifier?.sizeTextModifier) { 46 | setFragmentResult( 47 | SingleVideoPicker.SINGLE_VIDEO_REQUEST_KEY, 48 | bundleOf(SingleVideoPicker.ON_SINGLE_VIDEO_PICK_KEY to it as VideoModel) 49 | ) 50 | onVideoPicked?.forVideo(it) 51 | dismissAllowingStateLoss() 52 | } 53 | } 54 | 55 | override val askForStoragePermission = 56 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { 57 | if (it) { 58 | videosVM.loadVideos(extensions = extensions) 59 | } else { 60 | Log.e(errorTag, "PERMISSION DENIED") 61 | dismissAllowingStateLoss() 62 | } 63 | } 64 | 65 | 66 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 67 | super.onViewCreated(view, savedInstanceState) 68 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 69 | askForStoragePermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE) 70 | } else { 71 | askForStoragePermission.launch(Manifest.permission.READ_MEDIA_VIDEO) 72 | } 73 | setupUIForSinglePicker( 74 | binding.gallery, 75 | singleAdapter, 76 | binding.title, 77 | binding.loadingIndicator, 78 | modifier?.loadingIndicatorTint, 79 | ::applyTitleModifications 80 | ) 81 | videosVM.videos.observe(viewLifecycleOwner) { 82 | setupList(singleAdapter, it, binding.noContentText, modifier?.noContentTextModifier) 83 | } 84 | handleUIIndicator(videosVM.loadingIndicator, binding.loadingIndicator) 85 | 86 | } 87 | 88 | override fun onDestroyView() { 89 | super.onDestroyView() 90 | onVideoPicked = null 91 | } 92 | 93 | override fun applyTitleModifications(appCompatTextView: AppCompatTextView) { 94 | modifier?.titleTextModifier?.applyTextParams(appCompatTextView) 95 | } 96 | 97 | override fun addModifier(modifier: BaseSinglePickerModifier) { 98 | arguments = bundleOf(modifierTag to modifier) 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/listeners/onVideoPicked.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.listeners 2 | 3 | import com.crazylegend.videopicker.videos.VideoModel 4 | 5 | 6 | /** 7 | * Created by crazy on 5/8/20 to long live and prosper ! 8 | */ 9 | internal interface onVideoPicked { 10 | fun forVideo(videoModel: VideoModel) 11 | } 12 | 13 | internal inline fun onVideoDSL(crossinline videoCallback: (video: VideoModel) -> Unit) = object : onVideoPicked { 14 | override fun forVideo(videoModel: VideoModel) { 15 | videoCallback(videoModel) 16 | } 17 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/listeners/onVideosPicked.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.listeners 2 | 3 | import com.crazylegend.videopicker.videos.VideoModel 4 | 5 | 6 | /** 7 | * Created by crazy on 5/8/20 to long live and prosper ! 8 | */ 9 | internal interface onVideosPicked { 10 | fun onVideosPicked(videos: List = emptyList()) 11 | } 12 | 13 | internal inline fun onVideosDSL(crossinline callback: (list: List) -> Unit) = object : onVideosPicked { 14 | override fun onVideosPicked(videos: List) { 15 | callback(videos) 16 | } 17 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/pickers/MultiVideoPicker.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.pickers 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.fragment.app.FragmentManager 6 | import com.crazylegend.core.dto.PickerConfig 7 | import com.crazylegend.core.modifiers.base.BaseMultiPickerModifier 8 | import com.crazylegend.core.setupModifier 9 | import com.crazylegend.extensions.setupManager 10 | import com.crazylegend.videopicker.consts.MULTI_PICKER_BOTTOM_SHEET 11 | import com.crazylegend.videopicker.consts.MULTI_PICKER_DIALOG 12 | import com.crazylegend.videopicker.dialogs.multi.MultiVideoPickerBottomSheetDialog 13 | import com.crazylegend.videopicker.listeners.onVideosDSL 14 | import com.crazylegend.videopicker.videos.VideoModel 15 | 16 | 17 | /** 18 | * Created by crazy on 5/9/20 to long live and prosper ! 19 | */ 20 | 21 | object MultiVideoPicker { 22 | 23 | const val MULTI_VIDEO_REQUEST_KEY = "multiVideosRequest" 24 | const val ON_MULTI_VIDEO_PICK_KEY = "onMultiVideosPicked" 25 | 26 | fun restoreListener(context: Context, videoList: (list: List) -> Unit = {}) { 27 | val manager = context.setupManager() 28 | when (val fragment = manager.findFragmentByTag(MULTI_PICKER_BOTTOM_SHEET) 29 | ?: manager.findFragmentByTag(MULTI_PICKER_DIALOG)) { 30 | is MultiVideoPickerBottomSheetDialog -> { 31 | fragment.onVideosPicked = onVideosDSL(videoList) 32 | } 33 | 34 | null -> { 35 | Log.e(MultiVideoPicker::class.java.name, "FRAGMENT NOT FOUND") 36 | } 37 | } 38 | } 39 | 40 | fun showPicker( 41 | context: Context, 42 | extensions: Array? = arrayOf(), 43 | pickerConfig: PickerConfig = PickerConfig(), 44 | modifier: BaseMultiPickerModifier.() -> Unit = {}, 45 | videoList: (list: List) -> Unit = {}, 46 | ) { 47 | val manager = context.setupManager() 48 | val setupModifier = setupModifier(modifier) 49 | with(MultiVideoPickerBottomSheetDialog()) { 50 | this.extensions = extensions 51 | this.pickerConfig = pickerConfig 52 | addModifier(setupModifier) 53 | onVideosPicked = onVideosDSL(videoList) 54 | show(manager, MULTI_PICKER_BOTTOM_SHEET) 55 | } 56 | } 57 | 58 | fun showPicker( 59 | fragmentManager: FragmentManager, 60 | extensions: Array? = arrayOf(), 61 | pickerConfig: PickerConfig = PickerConfig(), 62 | modifier: BaseMultiPickerModifier.() -> Unit = {}, 63 | videoList: (list: List) -> Unit = {} 64 | ) { 65 | val setupModifier = setupModifier(modifier) 66 | with(MultiVideoPickerBottomSheetDialog()) { 67 | this.extensions = extensions 68 | this.pickerConfig = pickerConfig 69 | addModifier(setupModifier) 70 | onVideosPicked = onVideosDSL(videoList) 71 | show(fragmentManager, MULTI_PICKER_BOTTOM_SHEET) 72 | } 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/pickers/SingleVideoPicker.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.pickers 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.fragment.app.FragmentManager 6 | import com.crazylegend.core.dto.PickerConfig 7 | import com.crazylegend.core.modifiers.base.BaseSinglePickerModifier 8 | import com.crazylegend.core.setupModifier 9 | import com.crazylegend.extensions.setupManager 10 | import com.crazylegend.videopicker.consts.SINGLE_PICKER_BOTTOM_SHEET 11 | import com.crazylegend.videopicker.consts.SINGLE_PICKER_DIALOG 12 | import com.crazylegend.videopicker.dialogs.single.SingleVideoPickerBottomSheetDialog 13 | import com.crazylegend.videopicker.listeners.onVideoDSL 14 | import com.crazylegend.videopicker.videos.VideoModel 15 | 16 | 17 | /** 18 | * Created by crazy on 5/9/20 to long live and prosper ! 19 | */ 20 | object SingleVideoPicker { 21 | 22 | const val SINGLE_VIDEO_REQUEST_KEY = "singleVideoRequest" 23 | const val ON_SINGLE_VIDEO_PICK_KEY = "onSingleVideoPicked" 24 | 25 | fun restoreListener(context: Context, onPickedVideo: (video: VideoModel) -> Unit = {}) { 26 | val manager = context.setupManager() 27 | when (val fragment = manager.findFragmentByTag(SINGLE_PICKER_BOTTOM_SHEET) 28 | ?: manager.findFragmentByTag(SINGLE_PICKER_DIALOG)) { 29 | is SingleVideoPickerBottomSheetDialog -> { 30 | fragment.onVideoPicked = onVideoDSL(onPickedVideo) 31 | } 32 | 33 | null -> { 34 | Log.e(SingleVideoPicker::class.java.name, "FRAGMENT NOT FOUND") 35 | } 36 | } 37 | } 38 | 39 | fun showPicker( 40 | context: Context, 41 | extensions: Array? = arrayOf(), 42 | pickerConfig: PickerConfig = PickerConfig(), 43 | pickerModifier: BaseSinglePickerModifier.() -> Unit = {}, 44 | onPickedVideo: (video: VideoModel) -> Unit = {} 45 | ) { 46 | val modifier = setupModifier(pickerModifier) 47 | val manager = context.setupManager() 48 | with(SingleVideoPickerBottomSheetDialog()) { 49 | this.extensions = extensions 50 | this.pickerConfig = pickerConfig 51 | addModifier(modifier) 52 | onVideoPicked = onVideoDSL(onPickedVideo) 53 | show(manager, SINGLE_PICKER_BOTTOM_SHEET) 54 | } 55 | } 56 | 57 | fun showPicker( 58 | fragmentManager: FragmentManager, 59 | extensions: Array? = arrayOf(), 60 | pickerConfig: PickerConfig = PickerConfig(), 61 | pickerModifier: BaseSinglePickerModifier.() -> Unit = {}, 62 | onPickedVideo: (video: VideoModel) -> Unit = {} 63 | ) { 64 | val modifier = setupModifier(pickerModifier) 65 | with(SingleVideoPickerBottomSheetDialog()) { 66 | this.extensions = extensions 67 | this.pickerConfig = pickerConfig 68 | addModifier(modifier) 69 | onVideoPicked = onVideoDSL(onPickedVideo) 70 | show(fragmentManager, SINGLE_PICKER_BOTTOM_SHEET) 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /videopicker/src/main/java/com/crazylegend/videopicker/videos/VideoModel.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker.videos 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import com.crazylegend.core.dto.BaseCursorModel 6 | import kotlinx.parcelize.Parcelize 7 | 8 | 9 | /** 10 | * Simple data class to hold information about an image included in the device's MediaStore. 11 | */ 12 | 13 | @Parcelize 14 | data class VideoModel( 15 | override val id: Long, 16 | override val displayName: String?, 17 | override val dateAdded: Long?, 18 | override val contentUri: Uri, 19 | override val dateModified: Long?, 20 | override val description: String?, 21 | override val size: Int?, 22 | override val width: Int?, 23 | override val height: Int?, 24 | val resolution: String?, 25 | val private: Int?, 26 | val tags: String? 27 | ) : BaseCursorModel(id, displayName, dateAdded, contentUri, dateModified, description, size, width, height), Parcelable { 28 | 29 | 30 | } -------------------------------------------------------------------------------- /videopicker/src/test/java/com/crazylegend/videopicker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.crazylegend.videopicker 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | --------------------------------------------------------------------------------