├── fallery ├── consumer-rules.pro ├── src │ ├── main │ │ ├── java │ │ │ └── ir │ │ │ │ └── mehdiyari │ │ │ │ └── fallery │ │ │ │ ├── models │ │ │ │ ├── CacheDir.kt │ │ │ │ ├── BucketType.kt │ │ │ │ ├── MediaBucket.kt │ │ │ │ ├── Media.kt │ │ │ │ └── FalleryStyleAttrs.kt │ │ │ │ ├── utils │ │ │ │ ├── EnumType.kt │ │ │ │ ├── FileUtils.kt │ │ │ │ ├── BaseViewModel.kt │ │ │ │ ├── SingleLiveEvent.kt │ │ │ │ ├── PermissionHelper.kt │ │ │ │ ├── ErrorLayout.kt │ │ │ │ ├── AbstractFeatureComponentHolder.kt │ │ │ │ ├── VideoMediaTypes.kt │ │ │ │ ├── Projections.kt │ │ │ │ ├── BitmapUtils.kt │ │ │ │ ├── ThumbUtils.kt │ │ │ │ ├── ViewModelFactories.kt │ │ │ │ ├── MediaStoreObserver.kt │ │ │ │ ├── AndroidUtils.kt │ │ │ │ └── FalleryUtils.kt │ │ │ │ ├── main │ │ │ │ ├── ui │ │ │ │ │ ├── MediaCountModel.kt │ │ │ │ │ ├── FalleryView.kt │ │ │ │ │ └── FalleryToolbarVisibilityController.kt │ │ │ │ ├── fallery │ │ │ │ │ ├── FalleryResult.kt │ │ │ │ │ ├── Fallery.kt │ │ │ │ │ └── FalleryOptions.kt │ │ │ │ └── di │ │ │ │ │ ├── component │ │ │ │ │ ├── FalleryCoreComponent.kt │ │ │ │ │ ├── FalleryCoreComponentBuilder.kt │ │ │ │ │ ├── FalleryActivityComponentBuilder.kt │ │ │ │ │ └── FalleryActivityComponent.kt │ │ │ │ │ ├── FalleryActivityComponentHolder.kt │ │ │ │ │ ├── FalleryCoreComponentHolder.kt │ │ │ │ │ └── module │ │ │ │ │ └── FalleryCoreModule.kt │ │ │ │ ├── buckets │ │ │ │ ├── bucketContent │ │ │ │ │ ├── BucketContentSpanCount.kt │ │ │ │ │ ├── preview │ │ │ │ │ │ ├── AbstractMediaPreviewFragment.kt │ │ │ │ │ │ ├── adapter │ │ │ │ │ │ │ └── MediaPreviewAdapter.kt │ │ │ │ │ │ └── PhotoPreviewFragment.kt │ │ │ │ │ ├── BaseBucketContentFragment.kt │ │ │ │ │ ├── content │ │ │ │ │ │ └── RecyclerViewTouchListener.kt │ │ │ │ │ └── BucketContentViewModel.kt │ │ │ │ └── bucketList │ │ │ │ │ ├── LoadingViewState.kt │ │ │ │ │ ├── adapter │ │ │ │ │ ├── MediaBucketDiffCallback.kt │ │ │ │ │ └── BucketListAdapter.kt │ │ │ │ │ └── BucketListViewModel.kt │ │ │ │ ├── imageLoader │ │ │ │ ├── PhotoDiminution.kt │ │ │ │ └── FalleryImageLoader.kt │ │ │ │ └── repo │ │ │ │ ├── AbstractMediaBucketProvider.kt │ │ │ │ ├── AbstractBucketContentProvider.kt │ │ │ │ └── BucketContentProvider.kt │ │ ├── res │ │ │ ├── values │ │ │ │ ├── ids.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── drawable │ │ │ │ ├── fallery_play_circle.xml │ │ │ │ ├── top_shadow.xml │ │ │ │ ├── gradient_back_bucket_info.xml │ │ │ │ ├── fallery_ic_cancel.xml │ │ │ │ ├── fallery_grid_mode.xml │ │ │ │ ├── fallery_linear_mode.xml │ │ │ │ ├── fallery_ic_play_arrow_black.xml │ │ │ │ ├── ic_arrow_next.xml │ │ │ │ ├── fallery_ic_back_arrow.xml │ │ │ │ ├── ic_video.xml │ │ │ │ ├── fallery_icon_camera.xml │ │ │ │ ├── fallery_ic_crop_rotate.xml │ │ │ │ ├── fallery_icon_send.xml │ │ │ │ └── ic_error.xml │ │ │ ├── layout │ │ │ │ ├── fragment_base_bucket_content.xml │ │ │ │ ├── caption_edit_text_layout.xml │ │ │ │ ├── fragment_photo_preview.xml │ │ │ │ ├── fragment_video_preview.xml │ │ │ │ ├── fragment_bucket_list.xml │ │ │ │ ├── fragment_bucket_content.xml │ │ │ │ ├── media_photo_item.xml │ │ │ │ ├── error_layout.xml │ │ │ │ ├── caption_layout.xml │ │ │ │ ├── grid_bucket_item_view.xml │ │ │ │ ├── linear_bucket_item_view.xml │ │ │ │ ├── activity_fallery.xml │ │ │ │ ├── media_video_item.xml │ │ │ │ └── fragment_preview.xml │ │ │ └── values-fa │ │ │ │ └── strings.xml │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── ir │ │ └── mehdiyari │ │ └── fallery │ │ ├── utils │ │ ├── FileUtilsKtTest.kt │ │ ├── FalleryUtilsKtTest.kt │ │ └── MediaStoreObserverTest.kt │ │ └── buckets │ │ └── ui │ │ └── bucketList │ │ └── BucketListViewModelTest.kt ├── CHANGELOG.MD └── build.gradle ├── assets └── demo.jpg ├── settings.gradle ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── example ├── src │ ├── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ │ ├── font │ │ │ │ └── default_font.ttf │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── xml │ │ │ │ ├── provider_path.xml │ │ │ │ └── network_security_config.xml │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── drawables.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── drawable │ │ │ │ ├── side_nav_bar.xml │ │ │ │ ├── background_caption.xml │ │ │ │ ├── ic_baseline_add_24.xml │ │ │ │ ├── ic_baseline_menu_24.xml │ │ │ │ ├── ic_gallery.xml │ │ │ │ └── ic_clear.xml │ │ │ ├── menu │ │ │ │ ├── bottom_app_menu.xml │ │ │ │ └── navigation_items.xml │ │ │ ├── anim │ │ │ │ └── rotate.xml │ │ │ └── layout │ │ │ │ ├── custom_fallery_edit_text.xml │ │ │ │ ├── navigation_view.xml │ │ │ │ ├── navigation_header.xml │ │ │ │ ├── media_item_view.xml │ │ │ │ └── activity_main.xml │ │ ├── java │ │ │ └── ir │ │ │ │ └── mehdiyari │ │ │ │ └── falleryExample │ │ │ │ ├── utils │ │ │ │ ├── NetBucketModel.kt │ │ │ │ ├── FalleryExample.kt │ │ │ │ ├── CustomGalleryApiService.kt │ │ │ │ ├── GlideImageLoader.kt │ │ │ │ └── MediaJsonAdapter.kt │ │ │ │ └── ui │ │ │ │ ├── customGallery │ │ │ │ ├── CustomOnlineBucketProvider.kt │ │ │ │ └── CustomOnlineBucketContentProvider.kt │ │ │ │ ├── BottomNavigationDrawerFragment.kt │ │ │ │ └── MediaAdapter.kt │ │ └── AndroidManifest.xml │ └── test │ │ └── java │ │ └── ir │ │ └── mehdiyari │ │ └── falleryExample │ │ └── utils │ │ └── MediaJsonAdapterTest.kt └── build.gradle ├── .gitignore ├── .github └── dependabot.yml ├── gradle.properties ├── gradlew.bat └── gradlew /fallery/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/assets/demo.jpg -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='Fallery' 2 | include ':example' 3 | include ':fallery' 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /example/src/main/res/font/default_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/font/default_font.ttf -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/models/CacheDir.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.models 2 | 3 | class CacheDir(val cacheDir: String) -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/EnumType.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | interface EnumType { 4 | var value: T 5 | } 6 | 7 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehdiyari/Fallery/HEAD/example/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/ui/MediaCountModel.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.ui 2 | 3 | internal data class MediaCountModel( 4 | val selectedCount: Int, 5 | val totalCount: Int 6 | ) -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/models/BucketType.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.models 2 | 3 | enum class BucketType { 4 | ONLY_VIDEO_BUCKETS, 5 | ONLY_PHOTO_BUCKETS, 6 | VIDEO_PHOTO_BUCKETS 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /local.properties 3 | /.idea/* 4 | .DS_Store 5 | /build/* 6 | /.gradle/* 7 | example/build/* 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | *.lock 12 | libs/ 13 | proguard-rules.pro 14 | /fallery/build/* -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/fallery/FalleryResult.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.fallery 2 | 3 | data class FalleryResult( 4 | val mediaPathList: List? = null, 5 | val caption: String? = null 6 | ) -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/buckets/bucketContent/BucketContentSpanCount.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.buckets.bucketContent 2 | 3 | internal data class BucketContentSpanCount( 4 | val portraitSpanCount: Int, 5 | val landScapeSpanCount: Int 6 | ) -------------------------------------------------------------------------------- /example/src/main/res/xml/provider_path.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/ui/FalleryView.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.ui 2 | 3 | internal sealed class FalleryView { 4 | object BucketList : FalleryView() 5 | data class BucketContent(val bucketId: Long, val bucketName: String) : FalleryView() 6 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/ui/FalleryToolbarVisibilityController.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.ui 2 | 3 | internal interface FalleryToolbarVisibilityController { 4 | fun showToolbar(withAnim: Boolean = false) 5 | fun hideToolbar(withAnim: Boolean = false) 6 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Apr 19 18:50:10 IRDT 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-8.2-bin.zip 7 | -------------------------------------------------------------------------------- /example/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mehdiyari.ir 5 | 6 | -------------------------------------------------------------------------------- /fallery/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FFC107 6 | #1E88E5 7 | 8 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/buckets/bucketList/LoadingViewState.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.buckets.bucketList 2 | 3 | internal sealed class LoadingViewState { 4 | object ShowLoading : LoadingViewState() 5 | object HideLoading : LoadingViewState() 6 | object Error : LoadingViewState() 7 | } -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_play_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | rebase-strategy: "disabled" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | rebase-strategy: "disabled" -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/imageLoader/PhotoDiminution.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.imageLoader 2 | 3 | data class PhotoDiminution(val width: Int, val height: Int) { 4 | fun isNotSet(): Boolean = widthIsNotSet() && heightIsNotSet() 5 | 6 | fun widthIsNotSet() = width == 0 7 | fun heightIsNotSet() = height == 0 8 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/models/MediaBucket.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.models 2 | 3 | import java.io.Serializable 4 | 5 | data class MediaBucket( 6 | val id: Long, 7 | val bucketPath: String, 8 | val displayName: String, 9 | val firstMediaThumbPath: String, 10 | val mediaCount: Int = 1 11 | ) : Serializable -------------------------------------------------------------------------------- /fallery/src/main/res/layout/fragment_base_bucket_content.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /fallery/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 140dp 5 | 120dp 6 | 60dp 7 | 45dp 8 | 9 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/repo/AbstractMediaBucketProvider.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.repo 2 | 3 | import ir.mehdiyari.fallery.models.BucketType 4 | import ir.mehdiyari.fallery.models.MediaBucket 5 | 6 | interface AbstractMediaBucketProvider { 7 | 8 | suspend fun getMediaBuckets(bucketType: BucketType): List 9 | 10 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | internal fun getFileExtensionFromPath(url: String): String? = try { 4 | url.lastIndexOf('.').let { 5 | if (it != -1) 6 | url.substring(it + 1) 7 | else 8 | null 9 | } 10 | } catch (ignored: Throwable) { 11 | null 12 | } -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/top_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /example/src/main/res/menu/bottom_app_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /example/src/main/res/anim/rotate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/gradient_back_bucket_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/background_caption.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/src/main/java/ir/mehdiyari/falleryExample/utils/NetBucketModel.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.utils 2 | 3 | import com.squareup.moshi.Json 4 | 5 | data class NetBucketModel( 6 | @field:Json(name = "id") val id: Long, 7 | @field:Json(name = "display_name") val displayName: String, 8 | @field:Json(name = "thumbnail_url") val thumbnail: String, 9 | @field:Json(name = "media_count") val mediaCount: Int 10 | ) 11 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_ic_cancel.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_grid_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_baseline_add_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_linear_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /fallery/src/test/java/ir/mehdiyari/fallery/utils/FileUtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | class FileUtilsKtTest { 8 | 9 | @Test 10 | fun getFileExtensionFromPath() { 11 | assertEquals("mp4", getFileExtensionFromPath("/storage/emulated/0/Downloads/121324654_VID.mp4")) 12 | assertEquals(null, getFileExtensionFromPath("/storage/emulated/0/Downloads/")) 13 | } 14 | } -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_baseline_menu_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/main/java/ir/mehdiyari/falleryExample/utils/FalleryExample.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.utils 2 | 3 | import android.app.Application 4 | 5 | class FalleryExample : Application() { 6 | 7 | companion object { 8 | var customGalleryApiService: CustomGalleryApiService? = null 9 | get() { 10 | if (field == null) 11 | field = CustomGalleryApiService.create() 12 | return field 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_ic_play_arrow_black.xml: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/ic_arrow_next.xml: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_gallery.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_ic_back_arrow.xml: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /fallery/src/main/res/layout/caption_edit_text_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/src/main/res/layout/custom_fallery_edit_text.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fallery/src/main/res/layout/fragment_photo_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/buckets/bucketList/adapter/MediaBucketDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.buckets.bucketList.adapter 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import ir.mehdiyari.fallery.models.MediaBucket 5 | 6 | internal class MediaBucketDiffCallback : DiffUtil.ItemCallback() { 7 | 8 | override fun areItemsTheSame(oldItem: MediaBucket, newItem: MediaBucket): Boolean = 9 | oldItem.id == newItem.id 10 | 11 | override fun areContentsTheSame(oldItem: MediaBucket, newItem: MediaBucket): Boolean = 12 | oldItem == newItem 13 | } -------------------------------------------------------------------------------- /example/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 8dp 6 | 176dp 7 | 8 | 45dp 9 | 8dp 10 | 11 | 35dp 12 | 6dp 13 | -------------------------------------------------------------------------------- /example/src/main/res/values/drawables.xml: -------------------------------------------------------------------------------- 1 | 2 | @android:drawable/ic_menu_camera 3 | @android:drawable/ic_menu_gallery 4 | @android:drawable/ic_menu_slideshow 5 | @android:drawable/ic_menu_manage 6 | @android:drawable/ic_menu_share 7 | @android:drawable/ic_menu_send 8 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_clear.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/ic_video.xml: -------------------------------------------------------------------------------- 1 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_icon_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/repo/AbstractBucketContentProvider.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.repo 2 | 3 | import ir.mehdiyari.fallery.models.BucketType 4 | import ir.mehdiyari.fallery.models.Media 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface AbstractBucketContentProvider { 8 | 9 | /** 10 | * get medias in bucket with [bucketId] based on [bucketType] 11 | * @param bucketType BucketType filtering type 12 | * @param bucketId Long id of bucket 13 | * @return Flow> return flow of List 14 | */ 15 | suspend fun getMediasOfBucket( 16 | bucketId: Long, 17 | bucketType: BucketType 18 | ): Flow> 19 | 20 | } -------------------------------------------------------------------------------- /example/src/main/java/ir/mehdiyari/falleryExample/ui/customGallery/CustomOnlineBucketProvider.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.ui.customGallery 2 | 3 | import ir.mehdiyari.fallery.models.BucketType 4 | import ir.mehdiyari.fallery.models.MediaBucket 5 | import ir.mehdiyari.fallery.repo.AbstractMediaBucketProvider 6 | import ir.mehdiyari.falleryExample.utils.FalleryExample 7 | 8 | class CustomOnlineBucketProvider : AbstractMediaBucketProvider { 9 | 10 | override suspend fun getMediaBuckets(bucketType: BucketType): List = FalleryExample.customGalleryApiService!!.getBucketList().map { 11 | MediaBucket(it.id, it.thumbnail, it.displayName, it.thumbnail, it.mediaCount) 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_ic_crop_rotate.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/di/component/FalleryCoreComponent.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.di.component 2 | 3 | import ir.mehdiyari.fallery.imageLoader.FalleryImageLoader 4 | import ir.mehdiyari.fallery.main.fallery.FalleryOptions 5 | import ir.mehdiyari.fallery.repo.AbstractBucketContentProvider 6 | import ir.mehdiyari.fallery.repo.AbstractMediaBucketProvider 7 | 8 | internal interface FalleryCoreComponent { 9 | 10 | fun provideFalleryOptions(): FalleryOptions 11 | 12 | fun provideImageLoader(): FalleryImageLoader 13 | 14 | fun provideBucketProvider(): AbstractMediaBucketProvider 15 | 16 | fun provideBucketContentProvider(): AbstractBucketContentProvider 17 | 18 | fun releaseCoreComponent() 19 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/di/component/FalleryCoreComponentBuilder.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.di.component 2 | 3 | import ir.mehdiyari.fallery.main.di.module.FalleryCoreModule 4 | import ir.mehdiyari.fallery.main.fallery.FalleryOptions 5 | 6 | internal class FalleryCoreComponentBuilder { 7 | 8 | private var falleryOptions: FalleryOptions? = null 9 | 10 | fun bindFalleryOptions(falleryOptions: FalleryOptions): FalleryCoreComponentBuilder { 11 | this.falleryOptions = falleryOptions 12 | return this 13 | } 14 | 15 | fun build(): FalleryCoreModule { 16 | require(falleryOptions != null) { "falleryOptions must not be null" } 17 | 18 | return FalleryCoreModule(falleryOptions!!) 19 | } 20 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.SupervisorJob 7 | import kotlinx.coroutines.cancel 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | internal open class BaseViewModel : ViewModel() { 11 | 12 | protected val viewModelScope by lazy { 13 | object : CoroutineScope { 14 | override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate 15 | } 16 | } 17 | 18 | override fun onCleared() { 19 | viewModelScope.cancel() 20 | super.onCleared() 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /example/src/main/java/ir/mehdiyari/falleryExample/ui/customGallery/CustomOnlineBucketContentProvider.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.ui.customGallery 2 | 3 | import ir.mehdiyari.fallery.models.BucketType 4 | import ir.mehdiyari.fallery.models.Media 5 | import ir.mehdiyari.fallery.repo.AbstractBucketContentProvider 6 | import ir.mehdiyari.falleryExample.utils.FalleryExample 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flow 9 | 10 | class CustomOnlineBucketContentProvider : AbstractBucketContentProvider { 11 | 12 | override suspend fun getMediasOfBucket(bucketId: Long, bucketType: BucketType): Flow> = flow { 13 | emit(FalleryExample.customGalleryApiService!!.getBucketsContentById("bucket_$bucketId.json")) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.Observer 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | internal class SingleLiveEvent : MutableLiveData() { 10 | private val mPending = AtomicBoolean(false) 11 | 12 | @MainThread 13 | override fun observe(owner: LifecycleOwner, observer: Observer) { 14 | super.observe(owner) { t -> 15 | if (mPending.compareAndSet(true, false)) { 16 | observer.onChanged(t) 17 | } 18 | } 19 | } 20 | 21 | @MainThread 22 | override fun setValue(t: T?) { 23 | mPending.set(true) 24 | super.setValue(t) 25 | } 26 | } -------------------------------------------------------------------------------- /fallery/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import android.content.pm.PackageManager 4 | import android.os.Build 5 | import androidx.appcompat.app.AppCompatActivity 6 | 7 | internal inline fun AppCompatActivity.permissionChecker( 8 | permissions: Array, 9 | onAllGranted: () -> Unit = {}, 10 | onDenied: (List) -> Unit = {} 11 | ) { 12 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 13 | val notGranted = mutableListOf() 14 | for (permission in permissions) { 15 | if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { 16 | notGranted.add(permission) 17 | } 18 | } 19 | 20 | if (notGranted.isNotEmpty()) { 21 | onDenied(notGranted) 22 | } else { 23 | onAllGranted() 24 | } 25 | } else { 26 | onAllGranted() 27 | } 28 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/di/FalleryActivityComponentHolder.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.di 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | import ir.mehdiyari.fallery.main.di.component.FalleryActivityComponent 5 | import ir.mehdiyari.fallery.main.di.component.FalleryActivityComponentBuilder 6 | import ir.mehdiyari.fallery.utils.AbstractFeatureComponentHolder 7 | 8 | internal object FalleryActivityComponentHolder : 9 | AbstractFeatureComponentHolder() { 10 | 11 | override fun componentCreator(activity: FragmentActivity): FalleryActivityComponent { 12 | return FalleryActivityComponentBuilder().plusFalleryActivity(falleryActivity = activity) 13 | .plusFalleryCoreComponent(FalleryCoreComponentHolder.getOrThrow()) 14 | .build() 15 | } 16 | 17 | override fun onDestroy() { 18 | this.getOrNull()?.releaseBucketListComponent() 19 | super.onDestroy() 20 | } 21 | } -------------------------------------------------------------------------------- /fallery/src/main/res/layout/fragment_video_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/src/main/res/layout/navigation_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/buckets/bucketContent/preview/AbstractMediaPreviewFragment.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.buckets.bucketContent.preview 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import ir.mehdiyari.fallery.models.Media 7 | 8 | internal abstract class AbstractMediaPreviewFragment : Fragment() { 9 | 10 | var onMediaPreviewClickListener: View.OnClickListener? = null 11 | 12 | companion object { 13 | @JvmName("fromVideo") 14 | fun from(video: Media.Video): AbstractMediaPreviewFragment = VideoPreviewFragment() 15 | .apply { 16 | arguments = Bundle().apply { 17 | putParcelable("video", video) 18 | } 19 | } 20 | 21 | @JvmName("fromPhoto") 22 | fun from(photo: Media.Photo): AbstractMediaPreviewFragment = PhotoPreviewFragment() 23 | .apply { 24 | arguments = Bundle().apply { 25 | putParcelable("photo", photo) 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/ErrorLayout.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.LinearLayout 7 | import androidx.appcompat.widget.AppCompatTextView 8 | import androidx.constraintlayout.widget.ConstraintLayout 9 | import ir.mehdiyari.fallery.R 10 | 11 | internal class ErrorLayout(context: Context, attributeSet: AttributeSet) : 12 | LinearLayout(context, attributeSet) { 13 | 14 | init { 15 | View.inflate(context, R.layout.error_layout, this) 16 | } 17 | 18 | fun show() { 19 | findViewById(R.id.constraintLayoutRootLayout).visibility = View.VISIBLE 20 | } 21 | 22 | fun hide() { 23 | findViewById(R.id.constraintLayoutRootLayout).visibility = View.GONE 24 | } 25 | 26 | fun setOnRetryClickListener(function: () -> Unit) { 27 | findViewById(R.id.textViewRetry).setOnClickListener { 28 | function.invoke() 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/AbstractFeatureComponentHolder.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import android.util.Log 4 | import androidx.fragment.app.FragmentActivity 5 | import java.lang.ref.WeakReference 6 | 7 | internal abstract class AbstractFeatureComponentHolder { 8 | 9 | private var component: T? = null 10 | private var activity: WeakReference? = null 11 | 12 | fun createOrGetComponent(activity: FragmentActivity): T { 13 | if (this.component == null) component = componentCreator(activity).also { 14 | this.activity = WeakReference(activity) 15 | } 16 | 17 | return component!! 18 | } 19 | 20 | abstract fun componentCreator(activity: FragmentActivity): T 21 | 22 | open fun onDestroy() { 23 | try { 24 | Log.d(FALLERY_LOG_TAG, "${this::class.simpleName} has been destroyed") 25 | this.component = null 26 | } catch (ignored: Throwable) { 27 | ignored.printStackTrace() 28 | } 29 | } 30 | 31 | fun getOrNull(): T? = this.component 32 | } -------------------------------------------------------------------------------- /fallery/src/main/res/drawable/fallery_icon_send.xml: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/main/java/ir/mehdiyari/falleryExample/utils/CustomGalleryApiService.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.utils 2 | 3 | import com.squareup.moshi.Moshi 4 | import ir.mehdiyari.fallery.models.Media 5 | import retrofit2.Retrofit 6 | import retrofit2.converter.moshi.MoshiConverterFactory 7 | import retrofit2.http.GET 8 | import retrofit2.http.Path 9 | 10 | interface CustomGalleryApiService { 11 | 12 | @GET("fallery/buckets.json") 13 | suspend fun getBucketList(): List 14 | 15 | @GET("fallery/{name}") 16 | suspend fun getBucketsContentById(@Path("name") name: String): List 17 | 18 | companion object { 19 | fun create(): CustomGalleryApiService { 20 | val moshi = Moshi.Builder().add(MediaJsonAdapterFactory()).build() 21 | val moshiConverterFactory = MoshiConverterFactory 22 | .create(moshi) 23 | return Retrofit.Builder() 24 | .baseUrl("http://mehdiyari.ir/") 25 | .addConverterFactory(moshiConverterFactory) 26 | .build().create(CustomGalleryApiService::class.java) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/VideoMediaTypes.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | internal enum class VideoMediaTypes constructor(override var value: Pair>) : 4 | EnumType>> { 5 | 6 | MP4( 7 | "video/mp4" to listOf( 8 | "mp4", 9 | "m4v" 10 | ) 11 | ), 12 | 13 | QUICKTIME( 14 | "video/quicktime" to listOf( 15 | "mov" 16 | ) 17 | ), 18 | 19 | THREEGPP( 20 | "video/3gpp" to listOf( 21 | "3gp", 22 | "3gpp" 23 | ) 24 | ), 25 | 26 | THREEGPP2( 27 | "video/3gpp2" to listOf( 28 | "3g2", 29 | "3gpp2" 30 | ) 31 | ), 32 | 33 | MKV( 34 | "video/x-matroska" to listOf( 35 | "mkv" 36 | ) 37 | ), 38 | 39 | WEBM( 40 | "video/webm" to listOf( 41 | "webm" 42 | ) 43 | ), 44 | 45 | TS( 46 | "video/mp2ts" to listOf( 47 | "ts" 48 | ) 49 | ), 50 | 51 | AVI( 52 | "video/avi" to listOf( 53 | "avi" 54 | ) 55 | ); 56 | } -------------------------------------------------------------------------------- /example/src/test/java/ir/mehdiyari/falleryExample/utils/MediaJsonAdapterTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.utils 2 | 3 | import ir.mehdiyari.fallery.models.Media 4 | import org.junit.Test 5 | 6 | class MediaJsonAdapterTest { 7 | 8 | private val json = """ 9 | [ 10 | { 11 | "type": "photo", 12 | "id": 12, 13 | "path": "http//mehdiyari.ir/fallery/4.jpg", 14 | "width": 1280, 15 | "height": 960 16 | }, 17 | { 18 | "type": "video", 19 | "path": "http//mehdiyari.ir/fallery/second_video.mp4", 20 | "duration": 128, 21 | "thumbnail": { 22 | "id": 12, 23 | "path": "http//mehdiyari.ir/fallery/second_video.jpg", 24 | "width": 1920, 25 | "height": 1080 26 | } 27 | } 28 | ] 29 | """.trimIndent() 30 | 31 | @Test 32 | fun fromJson() { 33 | val jsonAdapter = MediaJsonAdapter() 34 | jsonAdapter.fromJson(json)?.apply { 35 | assert(this.size == 2) 36 | assert(this.first() is Media.Photo) 37 | assert(this[1] is Media.Video) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/di/component/FalleryActivityComponentBuilder.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.di.component 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | import ir.mehdiyari.fallery.main.di.module.FalleryActivityModule 5 | 6 | 7 | internal class FalleryActivityComponentBuilder { 8 | 9 | private var falleryActivity: FragmentActivity? = null 10 | private var falleryCoreComponent: FalleryCoreComponent? = null 11 | 12 | fun plusFalleryActivity(falleryActivity: FragmentActivity): FalleryActivityComponentBuilder = 13 | this.apply { 14 | this.falleryActivity = falleryActivity 15 | } 16 | 17 | fun plusFalleryCoreComponent(falleryCoreComponent: FalleryCoreComponent) : FalleryActivityComponentBuilder = this.apply { 18 | this.falleryCoreComponent = falleryCoreComponent 19 | } 20 | 21 | fun build(): FalleryActivityModule { 22 | require(falleryActivity != null) { "falleryActivity must set " } 23 | require(falleryCoreComponent != null) { "falleryCoreComponent must set " } 24 | return FalleryActivityModule( 25 | falleryActivity!!.applicationContext, falleryActivity!!, falleryCoreComponent!! 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/di/FalleryCoreComponentHolder.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.di 2 | 3 | import ir.mehdiyari.fallery.main.fallery.FalleryOptions 4 | import ir.mehdiyari.fallery.main.di.component.FalleryCoreComponent 5 | import ir.mehdiyari.fallery.main.di.component.FalleryCoreComponentBuilder 6 | 7 | internal object FalleryCoreComponentHolder { 8 | 9 | private var falleryCoreComponent: FalleryCoreComponent? = null 10 | 11 | fun createComponent(falleryOptions: FalleryOptions) { 12 | if (falleryCoreComponent == null) 13 | falleryCoreComponent = FalleryCoreComponentBuilder() 14 | .bindFalleryOptions(falleryOptions).build() 15 | } 16 | 17 | fun getOrThrow(): FalleryCoreComponent { 18 | require(falleryCoreComponent != null) { 19 | "falleryCoreComponent can't be null. please just use Fallery.startFalleryInActivity or Fallery.startFalleryInFragment for starting fallery" 20 | } 21 | return falleryCoreComponent!! 22 | } 23 | 24 | /** 25 | * this method must be called when fallery views(all) destroyed 26 | */ 27 | fun onDestroy() { 28 | falleryCoreComponent?.releaseCoreComponent() 29 | falleryCoreComponent = null 30 | } 31 | } -------------------------------------------------------------------------------- /fallery/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Gallery 3 | Caption… 4 | %1$d medias 5 | Camera 6 | List mode 7 | Require storage permission in order to choosing photo or video 8 | Storage permission has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Storage\". 9 | Continue 10 | Exit 11 | Cancel 12 | of 13 | selected 14 | Please install camera app first 15 | Something went wrong 16 | No media player found on device 17 | You can only choose %1$d Media 18 | Retry 19 | -------------------------------------------------------------------------------- /fallery/src/main/res/layout/fragment_bucket_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 16 | 17 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /fallery/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | گالری 3 | پیام… 4 | %1$d مدیا 5 | دوربین 6 | حالت نمایش لیست 7 | برنامه برای انتخاب مدیا نیاز به دسترسی به فضای ذخیره سازی دارد. 8 | دسترسی به فضای ذخیره سازی محدود شده است. لطفا به تنظیمات اپلیکیشن برورید و روی گزینه /"مجوز ها/" ضربه بزنید و سپس گزینه مجوز /"فضای ذخیره سازی/" را فعال کنید 9 | ادامه 10 | خروج 11 | لغو 12 | از 13 | انتخاب شده است 14 | لطفا ابتدا یک اپلیکیشن دوربین نصب کنید 15 | مشکلی پیش امده است 16 | هیچ پخش کننده ویدیویی برروی دستگاه پیدا نشد. 17 | شما فقط میتوانید %1$d مدیا را انتخاب کنید. 18 | تلاش مجدد 19 | -------------------------------------------------------------------------------- /fallery/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/src/main/java/ir/mehdiyari/falleryExample/utils/GlideImageLoader.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.utils 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.ColorDrawable 5 | import android.widget.ImageView 6 | import com.bumptech.glide.Glide 7 | import ir.mehdiyari.fallery.imageLoader.FalleryImageLoader 8 | import ir.mehdiyari.fallery.imageLoader.PhotoDiminution 9 | 10 | class GlideImageLoader : FalleryImageLoader { 11 | 12 | override fun loadPhoto( 13 | context: Context, 14 | imageView: ImageView, 15 | resizeDiminution: PhotoDiminution, 16 | placeHolderColor: Int, 17 | path: String 18 | ) { 19 | Glide.with(imageView) 20 | .asBitmap() 21 | .placeholder(ColorDrawable(placeHolderColor)) 22 | .load(path) 23 | .override(resizeDiminution.width, resizeDiminution.height) 24 | .into(imageView) 25 | } 26 | 27 | override fun loadGif( 28 | context: Context, 29 | imageView: ImageView, 30 | resizeDiminution: PhotoDiminution, 31 | placeHolderColor: Int, 32 | path: String 33 | ) { 34 | Glide.with(imageView) 35 | .asGif() 36 | .placeholder(ColorDrawable(placeHolderColor)) 37 | .load(path) 38 | .override(resizeDiminution.width, resizeDiminution.height) 39 | .into(imageView) 40 | } 41 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/imageLoader/FalleryImageLoader.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.imageLoader 2 | 3 | import android.content.Context 4 | import android.widget.ImageView 5 | 6 | /** 7 | * interface for loading photos and gif 8 | */ 9 | interface FalleryImageLoader { 10 | 11 | /** 12 | * load photo in [path] into [imageView] with [resizeDiminution] 13 | * this method called for loading thumbnails and photos 14 | * 15 | * @param context context 16 | * @param resizeDiminution requested width and height of photo 17 | * @param imageView photo load destination 18 | * @param path path of photo 19 | */ 20 | fun loadPhoto( 21 | context: Context, 22 | imageView: ImageView, 23 | resizeDiminution: PhotoDiminution, 24 | placeHolderColor: Int, 25 | path: String 26 | ) 27 | 28 | /** 29 | * load gif in [path] into [imageView] with [resizeDiminution] 30 | * this method called for loading gif photos 31 | * 32 | * @param context context 33 | * @param imageView gif load destination 34 | * @param resizeDiminution requested width and height of photo 35 | * @param path path of gif photo 36 | */ 37 | fun loadGif( 38 | context: Context, 39 | imageView: ImageView, 40 | resizeDiminution: PhotoDiminution, 41 | placeHolderColor: Int, 42 | path: String 43 | ) 44 | 45 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/buckets/bucketContent/preview/adapter/MediaPreviewAdapter.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.buckets.bucketContent.preview.adapter 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.viewpager2.adapter.FragmentStateAdapter 7 | import ir.mehdiyari.fallery.buckets.bucketContent.preview.AbstractMediaPreviewFragment 8 | import ir.mehdiyari.fallery.buckets.bucketContent.preview.PreviewFragment 9 | import ir.mehdiyari.fallery.models.Media 10 | 11 | internal class MediaPreviewAdapter( 12 | previewFragment: PreviewFragment, 13 | private val onViewPagerClick: View.OnClickListener? = null 14 | ) : FragmentStateAdapter(previewFragment) { 15 | 16 | var medias: List? = null 17 | @SuppressLint("NotifyDataSetChanged") 18 | set(value) { 19 | field = value 20 | notifyDataSetChanged() 21 | } 22 | 23 | override fun getItemCount(): Int = medias?.size ?: 0 24 | 25 | override fun createFragment(position: Int): Fragment = (medias?.getOrNull(position)?.let { 26 | when (it) { 27 | is Media.Photo -> AbstractMediaPreviewFragment.from(it) 28 | is Media.Video -> AbstractMediaPreviewFragment.from(it) 29 | } 30 | } ?: AbstractMediaPreviewFragment.from(Media.Photo.empty())).apply { 31 | onMediaPreviewClickListener = onViewPagerClick 32 | } 33 | } -------------------------------------------------------------------------------- /fallery/src/main/res/layout/fragment_bucket_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /fallery/src/test/java/ir/mehdiyari/fallery/utils/FalleryUtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import android.media.MediaMetadataRetriever 4 | import com.google.common.truth.Truth.assertThat 5 | import io.mockk.every 6 | import io.mockk.mockk 7 | import io.mockk.verify 8 | import org.junit.jupiter.api.Test 9 | 10 | internal class FalleryUtilsKtTest { 11 | 12 | @Test 13 | fun ` Given 765 - when call toReadableCount - then return "765" `() { 14 | assertThat(765.toReadableCount()).isEqualTo("765") 15 | } 16 | 17 | @Test 18 | fun ` Given 500_000 - when call toReadableCount - then return 500K `() { 19 | assertThat(500_000.toReadableCount()).isEqualTo("500K") 20 | } 21 | 22 | @Test 23 | fun ` Given 653_352_532 - when call toReadableCount - then return 653M `() { 24 | assertThat(653_352_532.toReadableCount()).isEqualTo("653M") 25 | } 26 | 27 | @Test 28 | fun `Given 73 seconds - when call convertSecondToTime - return 1min 13seconds`() { 29 | assertThat(convertSecondToTime(73)).isEqualTo("01:13") 30 | } 31 | 32 | @Test 33 | fun `Given 3673 seconds - when call convertSecondToTime - return 1hour 1min 13seconds`() { 34 | assertThat(convertSecondToTime(3673)).isEqualTo("01:01:13") 35 | } 36 | 37 | @Test 38 | fun `when call autoClose - then verify release called`() { 39 | val mockedMediaMetadataRetriever: MediaMetadataRetriever = mockk() 40 | every { mockedMediaMetadataRetriever.release() } returns Unit 41 | mockedMediaMetadataRetriever.autoClose { println("do some works...") } 42 | verify(exactly = 1) { mockedMediaMetadataRetriever.release() } 43 | } 44 | } -------------------------------------------------------------------------------- /fallery/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #151e27 6 | #212d3b 7 | #151e27 8 | #5ea3de 9 | #7a8694 10 | #ffffff 11 | #ffffff 12 | #7c8896 13 | #697686 14 | #1d2733 15 | #FFFFFF 16 | 17 | 18 | #ffffff 19 | #ffffff 20 | #CFD8DC 21 | #A11183 22 | #504f4f 23 | #504f4f 24 | #000000 25 | #999999 26 | #999999 27 | #EEEEEE 28 | #000000 29 | 30 | 31 | #000000 32 | -------------------------------------------------------------------------------- /example/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 16 | 17 | 18 | 36 | 37 | -------------------------------------------------------------------------------- /example/src/main/res/layout/navigation_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 25 | 26 | 39 | 40 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/di/component/FalleryActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.di.component 2 | 3 | import android.content.ContentResolver 4 | import android.graphics.drawable.Drawable 5 | import androidx.fragment.app.FragmentActivity 6 | import ir.mehdiyari.fallery.buckets.bucketContent.content.adapter.BucketContentAdapter 7 | import ir.mehdiyari.fallery.buckets.bucketList.adapter.BucketListAdapter 8 | import ir.mehdiyari.fallery.buckets.bucketList.adapter.MediaBucketDiffCallback 9 | import ir.mehdiyari.fallery.models.CacheDir 10 | import ir.mehdiyari.fallery.models.FalleryStyleAttrs 11 | import ir.mehdiyari.fallery.utils.BucketContentViewModelFactory 12 | import ir.mehdiyari.fallery.utils.BucketListViewModelFactory 13 | import ir.mehdiyari.fallery.utils.FalleryViewModelFactory 14 | import ir.mehdiyari.fallery.utils.MediaStoreObserver 15 | 16 | internal interface FalleryActivityComponent : FalleryCoreComponent { 17 | 18 | fun provideBucketListViewModelFactory(): BucketListViewModelFactory 19 | 20 | fun provideActivity(): FragmentActivity 21 | 22 | fun releaseBucketListComponent() 23 | 24 | fun provideMediaBucketDiffCallback(): MediaBucketDiffCallback 25 | 26 | fun provideBucketContentAdapter(): BucketContentAdapter 27 | 28 | fun provideBucketContentViewModelFactory(): BucketContentViewModelFactory 29 | 30 | fun provideFalleryStyleAttrs(): FalleryStyleAttrs 31 | 32 | fun provideCacheDir(): CacheDir 33 | 34 | fun provideContentResolver(): ContentResolver 35 | 36 | fun provideFalleryViewModelFactory(): FalleryViewModelFactory 37 | 38 | fun provideSelectedDrawable(): Drawable 39 | 40 | fun provideDeselectedDrawable(): Drawable 41 | 42 | fun provideBucketListAdapter(): BucketListAdapter 43 | 44 | fun provideMediaStoreObserver(): MediaStoreObserver 45 | } -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 12 | 13 | 14 | 15 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/main/di/module/FalleryCoreModule.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.main.di.module 2 | 3 | import ir.mehdiyari.fallery.imageLoader.FalleryImageLoader 4 | import ir.mehdiyari.fallery.main.fallery.FalleryOptions 5 | import ir.mehdiyari.fallery.main.di.component.FalleryCoreComponent 6 | import ir.mehdiyari.fallery.repo.AbstractBucketContentProvider 7 | import ir.mehdiyari.fallery.repo.AbstractMediaBucketProvider 8 | import ir.mehdiyari.fallery.repo.MediaBucketProvider 9 | import java.lang.NullPointerException 10 | 11 | internal class FalleryCoreModule constructor( 12 | private val falleryOptions: FalleryOptions 13 | ) : FalleryCoreComponent { 14 | 15 | private var defaultImageLoader: FalleryImageLoader? = null 16 | private var abstractMediaBucketProvider: AbstractMediaBucketProvider? = null 17 | private var abstractBucketContentProvider: AbstractBucketContentProvider? = null 18 | 19 | override fun provideFalleryOptions(): FalleryOptions = falleryOptions 20 | 21 | override fun provideImageLoader(): FalleryImageLoader = falleryOptions.imageLoader ?: throw NullPointerException("imageLoader must not be null") 22 | 23 | override fun provideBucketProvider(): AbstractMediaBucketProvider = 24 | falleryOptions.bucketProviderAbstract ?: throw IllegalArgumentException("${MediaBucketProvider::class.simpleName} can provide only in feature component holders") 25 | 26 | override fun provideBucketContentProvider(): AbstractBucketContentProvider = falleryOptions.abstractBucketContentProvider 27 | ?: throw IllegalArgumentException("${AbstractBucketContentProvider::class.simpleName} can provide only in feature component holders") 28 | 29 | override fun releaseCoreComponent() { 30 | defaultImageLoader = null 31 | abstractMediaBucketProvider = null 32 | abstractBucketContentProvider = null 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/Projections.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import android.provider.MediaStore 4 | import ir.mehdiyari.fallery.models.BucketType 5 | 6 | 7 | internal val bucketProjection = arrayOf( 8 | MediaStore.Files.FileColumns._ID, 9 | "bucket_id", 10 | "bucket_display_name", 11 | MediaStore.MediaColumns.DATA, 12 | MediaStore.MediaColumns.MIME_TYPE, 13 | "COUNT(*) AS count", 14 | "datetaken" 15 | ) 16 | 17 | internal val photoWithVideoProjection = arrayOf( 18 | MediaStore.Files.FileColumns._ID, 19 | MediaStore.Files.FileColumns.DATA, 20 | MediaStore.Files.FileColumns.SIZE, 21 | MediaStore.Files.FileColumns.MIME_TYPE, 22 | MediaStore.Files.FileColumns.MEDIA_TYPE, 23 | MediaStore.Files.FileColumns.DATE_ADDED 24 | ) 25 | 26 | 27 | internal val bucketProjectionAndroidQ = arrayOf( 28 | MediaStore.Files.FileColumns._ID, 29 | "bucket_id", 30 | "bucket_display_name", 31 | MediaStore.MediaColumns.DATA, 32 | MediaStore.MediaColumns.MIME_TYPE, 33 | "datetaken" 34 | ) 35 | 36 | internal fun getQueryByMediaType(mediaType: BucketType): String = when (mediaType) { 37 | BucketType.VIDEO_PHOTO_BUCKETS -> if (isAndroidTenOrHigher()) videoPhotoBucketSelectionAndroidQ else videoPhotoBucketSelection 38 | else -> if (isAndroidTenOrHigher()) getSingleBucketSelectionAndroidQ else getSingleBucketSelection 39 | } 40 | 41 | internal fun getQueryArgByMediaType(mediaType: BucketType): Array = when (mediaType) { 42 | BucketType.VIDEO_PHOTO_BUCKETS -> arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()) 43 | BucketType.ONLY_PHOTO_BUCKETS -> arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString()) 44 | BucketType.ONLY_VIDEO_BUCKETS -> arrayOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()) 45 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/BitmapUtils.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.graphics.drawable.Drawable 6 | import android.graphics.drawable.GradientDrawable 7 | import android.media.MediaMetadataRetriever 8 | import androidx.annotation.ColorInt 9 | import ir.mehdiyari.fallery.imageLoader.PhotoDiminution 10 | import java.io.FileOutputStream 11 | 12 | internal fun getPhotoDimension(path: String): PhotoDiminution = BitmapFactory.Options().apply { 13 | inJustDecodeBounds = true 14 | BitmapFactory.decodeFile(path, this) 15 | }.let { 16 | PhotoDiminution(it.outWidth, it.outHeight) 17 | } 18 | 19 | internal fun Bitmap.saveBitmapToFile( 20 | cachePath: String, 21 | format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, 22 | quality: Int = 100 23 | ) { 24 | FileOutputStream(cachePath).also { output -> 25 | this.compress(format, quality, output) 26 | } 27 | } 28 | 29 | internal fun MediaMetadataRetriever.getVideoSize(): PhotoDiminution = PhotoDiminution( 30 | extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 150, 31 | extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 150 32 | ) 33 | 34 | internal fun getHeightBasedOnScaledWidth( 35 | originalWidth: Int, 36 | originalHeight: Int, 37 | scaledWidth: Int 38 | ): Int = (originalHeight / (originalWidth.toFloat() / scaledWidth)).toInt() 39 | 40 | internal fun createCircleDrawableWithStroke( 41 | @ColorInt backgroundColor: Int, 42 | strokeWidth: Int, 43 | @ColorInt strokeColor: Int 44 | ): Drawable { 45 | val defaultDrawable = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(backgroundColor, backgroundColor)) 46 | defaultDrawable.cornerRadius = 300f 47 | defaultDrawable.setStroke(strokeWidth, strokeColor) 48 | return defaultDrawable 49 | } -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/utils/ThumbUtils.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.utils 2 | 3 | import android.graphics.Bitmap 4 | import android.media.ThumbnailUtils 5 | import android.os.Build 6 | import android.provider.MediaStore 7 | import android.util.Size 8 | import androidx.annotation.WorkerThread 9 | import ir.mehdiyari.fallery.imageLoader.PhotoDiminution 10 | import java.io.File 11 | 12 | 13 | @WorkerThread 14 | internal fun createThumbForVideos( 15 | videosPath: List>, 16 | cacheDir: String, 17 | highQuality: Pair = false to null 18 | ): List = mutableListOf().apply { 19 | videosPath.forEach { 20 | val cachePath = 21 | "$cacheDir/${File(it.first).nameWithoutExtension}__${it.second}.jpg" 22 | if (File(cachePath).exists()) 23 | add(cachePath) 24 | else { 25 | (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 26 | ThumbnailUtils.createVideoThumbnail( 27 | File(it.first), 28 | if (!highQuality.first) Size(512, 384) else Size(highQuality.second!!.width, highQuality.second!!.height), 29 | null 30 | ) 31 | } else { 32 | ThumbnailUtils.createVideoThumbnail( 33 | it.first, 34 | if (highQuality.first) MediaStore.Images.Thumbnails.FULL_SCREEN_KIND else MediaStore.Images.Thumbnails.MINI_KIND 35 | ) 36 | })?.apply { 37 | saveBitmapToFile( 38 | cachePath, Bitmap.CompressFormat.JPEG, 75 39 | ) 40 | } 41 | 42 | add(cachePath) 43 | } 44 | } 45 | }.toList() 46 | 47 | internal fun createThumbForVideosOrEmpty( 48 | videosPath: List>, 49 | cacheDir: String, 50 | highQuality: Pair = false to null 51 | ): List = try { 52 | createThumbForVideos(videosPath, cacheDir, highQuality) 53 | } catch (ignored: Throwable) { 54 | ignored.printStackTrace() 55 | listOf("") 56 | } -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | namespace "ir.mehdiyari.falleryExample" 7 | compileSdkVersion 34 8 | 9 | 10 | defaultConfig { 11 | applicationId "ir.mehdiyari.falleryExample" 12 | minSdkVersion 14 13 | targetSdkVersion 34 14 | versionCode 2 15 | versionName "1.0.0" 16 | multiDexEnabled true 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | kotlinOptions { 28 | jvmTarget = JavaVersion.VERSION_1_8.toString() 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | buildFeatures { 37 | viewBinding true 38 | } 39 | 40 | kotlinOptions { 41 | jvmTarget = '1.8' 42 | } 43 | 44 | } 45 | 46 | dependencies { 47 | implementation fileTree(dir: 'libs', include: ['*.jar']) 48 | implementation libs.kotlin.stdlib.jdk7 49 | implementation libs.appcompat 50 | implementation libs.core.ktx 51 | implementation libs.androidx.constraintlayout 52 | testImplementation libs.junit 53 | androidTestImplementation libs.androidx.test.ext.junit 54 | androidTestImplementation libs.espresso.core 55 | 56 | implementation libs.material 57 | implementation project(":fallery") 58 | implementation libs.kotlinx.coroutines.android 59 | implementation libs.kotlinx.coroutines.core 60 | 61 | implementation libs.glide 62 | kapt libs.compiler 63 | implementation libs.multidex 64 | implementation libs.retrofit 65 | implementation libs.converter.moshi 66 | implementation libs.roundedimageview 67 | 68 | debugImplementation libs.leakcanary.android 69 | implementation libs.leakcanary.object.watcher.android 70 | } 71 | -------------------------------------------------------------------------------- /example/src/main/res/layout/media_item_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 25 | 26 | 33 | 34 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /fallery/src/main/res/layout/media_photo_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 20 | 31 | 32 | 33 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /example/src/main/java/ir/mehdiyari/falleryExample/ui/BottomNavigationDrawerFragment.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.falleryExample.ui 2 | 3 | import android.os.Bundle 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.view.LayoutInflater 7 | import android.view.MenuItem 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.core.view.forEach 11 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 12 | import ir.mehdiyari.falleryExample.R 13 | import ir.mehdiyari.falleryExample.databinding.NavigationViewBinding 14 | 15 | class BottomNavigationDrawerFragment : BottomSheetDialogFragment() { 16 | 17 | private var _binding: NavigationViewBinding? = null 18 | private val binding get() = _binding!! 19 | 20 | var selectedItemId: Int = R.id.menuDefaultOptions 21 | 22 | var onMenuItemSelected: ((itemId: Int) -> Unit)? = null 23 | 24 | override fun onCreateView( 25 | inflater: LayoutInflater, 26 | container: ViewGroup?, 27 | savedInstanceState: Bundle? 28 | ): View = NavigationViewBinding.inflate(layoutInflater).also { 29 | _binding = it 30 | }.root 31 | 32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 33 | super.onViewCreated(view, savedInstanceState) 34 | binding.navigationViewExample.menu.findItem(selectedItemId).isChecked = true 35 | binding.navigationViewExample.setNavigationItemSelectedListener { 36 | handleOnItemClick(it) 37 | true 38 | } 39 | } 40 | 41 | private fun handleOnItemClick(menuItem: MenuItem) { 42 | binding.navigationViewExample.menu.forEach { 43 | if (it.isChecked) { 44 | it.isChecked = false 45 | return@forEach 46 | } 47 | } 48 | 49 | menuItem.isChecked = true 50 | Handler(Looper.getMainLooper()).postDelayed({ 51 | onMenuItemSelected?.invoke(menuItem.itemId) 52 | dismiss() 53 | }, 400) 54 | } 55 | 56 | override fun onDestroyView() { 57 | binding.navigationViewExample.setNavigationItemSelectedListener(null) 58 | _binding = null 59 | super.onDestroyView() 60 | } 61 | } -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Fallery 3 | Gallery and media picker for Android 4 | Default Options 5 | Media Observer Enabled 6 | With Caption 7 | Without Caption 8 | Dracula Theme 9 | Light Theme 10 | Media Filter Type | Only Photos 11 | Camera Enabled 12 | Landscape Orientation 13 | Bucket List Mode Grid 14 | ViewPager Vertical Orientation 15 | Selected Media Toggle Color 16 | Custom Caption EditText 17 | Custom Video Toggle OnClick 18 | Max Selectable 19 | Clear 20 | No media selected. Tap + to open Fallery and select media 21 | Bucket List Mode Linear 22 | Media Filter Type | Only Videos 23 | Custom Gallery Online 24 | Custom Theme 25 | Change span count with zoom-in and zoom-out 26 | 27 | -------------------------------------------------------------------------------- /fallery/CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | ## Fallery version 1.0.4 2 | 3 | 1. Support starting fallery from composable functions 4 | 2. Minor bug fixes 5 | 6 | ## Fallery version 1.0.3 7 | 8 | 1. Update Dependencies 9 | 2. Remove MANAGE_EXTERNAL_STORAGE permission 10 | 3. Support Android +13 by 11 | 4. Add a new publish task 12 | 5. Migrate to view binding 13 | 6. Minor bug fixes 14 | 15 | ## Fallery version 1.0.2 16 | 17 | 1. Update library versions 18 | 2. Fix issues of preview fragment after device rotate 19 | 20 | ## Fallery version 1.0.1 21 | 22 | 1. CleanUp codes 23 | 2. Update dependencies 24 | 3. Fix some bugs 25 | 4. Add a new option to fallery for changing bucket content span-count based on user touch(zoom in, zoom out) 26 | 5. Update README.MD 27 | 28 | ## Fallery version 1.0.0 29 | 30 | #### Compatible with android +10 31 | 1. Package visibility in Android 11 32 | 2. Storage updates in Android 11 33 | 34 | #### Fix Bugs 35 | 1. Finish fallery activity if CoreComponent is null(if permissions changed from settings or any case that fallery started without official API) 36 | 2. Fix issue of showing error layout when views is on loading state 37 | 3. Fix issue of creating two instance of FalleryActivityComponent 38 | 4. Fix issue of media store observable 39 | 5. Fix bug of resetting viewpager adapter position after onStop 40 | 7. Fix animation of sending media footer 41 | 8. Fix memory leak of gridLayoutManagers in BucketContentFragment & BucketListFragment 42 | 9. Fix lint errors and warnings 43 | 10. Fix memory leak of bottomNavigationDrawerFragment in MainActivity 44 | 11. Fix failed tests in BucketListViewModelTest.kt 45 | 12. Remove unused classes 46 | 47 | #### Update third-party libraries 48 | 1. Update gradle to 6.8.3 49 | 2. Update kotlin version to 1.4.32 50 | 3. Update android gradle plugin to 4.1.3 51 | 4. Update fallery example app dependencies to latest versions 52 | 5. Run kotlin migration to 1.4.32 53 | 6. Update fallery libraries version 54 | 7. Update compile and target sdk to 30 55 | 8. Update kotlinx-coroutines-test dependency to 1.4.1 56 | 57 | ## Fallery version 0.9.2 58 | 59 | Code cleanup and optimization 60 | 61 | ## Fallery version 0.9.1 62 | 63 | Fix issue When the screen rotate. The current photo is not displayed
64 | Fix the issue of showing recyclerViewItemMode menuItem
65 | 66 | ## Fallery version 0.9.0 67 | 68 | First Release 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fallery/src/main/java/ir/mehdiyari/fallery/models/Media.kt: -------------------------------------------------------------------------------- 1 | package ir.mehdiyari.fallery.models 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import ir.mehdiyari.fallery.utils.getFileExtensionFromPath 6 | import java.io.Serializable 7 | 8 | sealed class Media { 9 | fun getMediaId(): Long = when (this) { 10 | is Photo -> id 11 | is Video -> id 12 | } 13 | 14 | fun getMediaPath(): String = when (this) { 15 | is Photo -> path 16 | is Video -> path 17 | } 18 | 19 | data class Photo( 20 | val id: Long, 21 | val path: String, 22 | val width: Int, 23 | val height: Int 24 | ) : Media(), Parcelable { 25 | 26 | fun isGif(): Boolean = getFileExtensionFromPath(path) == "gif" 27 | 28 | constructor(parcel: Parcel) : this(parcel.readLong(), parcel.readString() ?: "", parcel.readInt(), parcel.readInt()) 29 | 30 | override fun writeToParcel(parcel: Parcel, flags: Int) { 31 | parcel.writeLong(id) 32 | parcel.writeString(path) 33 | parcel.writeInt(width) 34 | parcel.writeInt(height) 35 | } 36 | 37 | override fun describeContents(): Int = 0 38 | 39 | companion object CREATOR : Parcelable.Creator { 40 | fun empty() = Photo(0, "", 0, 0) 41 | override fun createFromParcel(parcel: Parcel): Photo = Photo(parcel) 42 | override fun newArray(size: Int): Array = arrayOfNulls(size) 43 | } 44 | } 45 | 46 | data class Video( 47 | val id: Long, 48 | val path: String, 49 | val duration: Long, 50 | val thumbnail: Photo 51 | ) : Media(), Serializable, Parcelable { 52 | 53 | constructor(parcel: Parcel) : this( 54 | parcel.readLong(), 55 | parcel.readString() ?: "", 56 | parcel.readLong(), 57 | parcel.readParcelable(Photo::class.java.classLoader)!! 58 | ) 59 | 60 | override fun writeToParcel(parcel: Parcel, flags: Int) { 61 | parcel.writeLong(id) 62 | parcel.writeString(path) 63 | parcel.writeLong(duration) 64 | parcel.writeParcelable(thumbnail, flags) 65 | } 66 | 67 | override fun describeContents(): Int = 0 68 | 69 | companion object CREATOR : Parcelable.Creator