├── 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 |
--------------------------------------------------------------------------------
/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
83 | *
84 | */
85 | @JvmName("getFalleryActivityResultContract")
86 | fun getFalleryActivityResultContract(): ActivityResultContract {
87 | return object : ActivityResultContract() {
88 | override fun createIntent(context: Context, input: FalleryOptions): Intent {
89 | FalleryCoreComponentHolder.createComponent(input)
90 | return Intent(context, FalleryActivity::class.java)
91 | }
92 |
93 | override fun parseResult(resultCode: Int, intent: Intent?): FalleryResult {
94 | return FalleryResult(
95 | intent?.getFalleryResultMediasFromIntent()?.toList(),
96 | intent?.getFalleryCaptionFromIntent()
97 | )
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/fallery/src/main/java/ir/mehdiyari/fallery/repo/BucketContentProvider.kt:
--------------------------------------------------------------------------------
1 | package ir.mehdiyari.fallery.repo
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.ContentResolver
5 | import android.database.Cursor
6 | import android.media.MediaMetadataRetriever
7 | import android.provider.MediaStore
8 | import ir.mehdiyari.fallery.imageLoader.PhotoDiminution
9 | import ir.mehdiyari.fallery.models.BucketType
10 | import ir.mehdiyari.fallery.models.CacheDir
11 | import ir.mehdiyari.fallery.models.Media
12 | import ir.mehdiyari.fallery.utils.*
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.flow
15 | import kotlin.random.Random
16 |
17 | internal class BucketContentProvider constructor(
18 | private val contentResolver: ContentResolver,
19 | private val cacheDir: CacheDir
20 | ) : AbstractBucketContentProvider {
21 |
22 | override suspend fun getMediasOfBucket(bucketId: Long, bucketType: BucketType): Flow> = flow {
23 | val medias = mutableListOf()
24 | val mediaMetadataRetriever = MediaMetadataRetriever()
25 | contentResolver.query(
26 | MediaStore.Files.getContentUri("external"),
27 | photoWithVideoProjection,
28 | "${getSimpleQueryByMediaType(bucketType)} ${if (bucketId == ALL_MEDIA_MODEL_ID) "" else "AND bucket_id=?"}",
29 | getQueryArgsForGetContentBuckets(bucketType, bucketId),
30 | "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
31 | )?.also { cursor ->
32 | if (cursor.count == 0) {
33 | emit(listOf())
34 | } else {
35 | while (cursor.moveToNext()) {
36 | medias.add(getMediaFromCursor(cursor, mediaMetadataRetriever))
37 | // emit medias as chunks with size 100
38 | if (medias.size >= 100) {
39 | emit(medias.toList())
40 | medias.clear()
41 | }
42 | }
43 | }
44 |
45 | emit(medias.toList())
46 | medias.clear()
47 | cursor.close()
48 | }
49 | }
50 |
51 | @SuppressLint("Range")
52 | private fun getMediaFromCursor(
53 | cursor: Cursor,
54 | mediaMetadataRetriever: MediaMetadataRetriever
55 | ): Media {
56 | val isPhoto = cursor.getInt(cursor.getColumnIndex(photoWithVideoProjection[4])) == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
57 | val path = cursor.getString(cursor.getColumnIndexOrThrow(photoWithVideoProjection[1]))
58 | if (!isPhoto) mediaMetadataRetriever.setDataSource(path)
59 |
60 | val thumbnail = if (!isPhoto)
61 | createThumbForVideosOrEmpty(listOf(Pair(path, cursor.getLong(cursor.getColumnIndex(photoWithVideoProjection[5])))), cacheDir.cacheDir).firstOrNull()
62 | else ""
63 |
64 | val id = cursor.getLong(cursor.getColumnIndex(photoWithVideoProjection[0]))
65 |
66 | return if (isPhoto) {
67 | getPhotoDimension(path).let {
68 | Media.Photo(id, path, it.width, it.height)
69 | }
70 | } else {
71 | val thumbnailDiminution = try {
72 | getPhotoDimension(thumbnail ?: "")
73 | } catch (ignored: Throwable) {
74 | PhotoDiminution(0, 0)
75 | }
76 |
77 | Media.Video(
78 | id, path, ((mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 1000L) / 1000), Media.Photo(
79 | Random.nextLong(), thumbnail ?: "", thumbnailDiminution.width, thumbnailDiminution.height
80 | )
81 | )
82 | }
83 | }
84 |
85 | private fun getSimpleQueryByMediaType(mediaType: BucketType): String = when (mediaType) {
86 | BucketType.VIDEO_PHOTO_BUCKETS -> "(${MediaStore.Files.FileColumns.MEDIA_TYPE}=? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE}=?)"
87 | else -> "${MediaStore.Files.FileColumns.MEDIA_TYPE}=?"
88 | }
89 |
90 | private fun getQueryArgsForGetContentBuckets(bucketType: BucketType, bucketId: Long): Array {
91 | val arrays = mutableListOf()
92 | arrays.addAll(getQueryArgByMediaType(bucketType))
93 | if (bucketId != ALL_MEDIA_MODEL_ID)
94 | arrays.add(bucketId.toString())
95 |
96 | return arrays.toTypedArray()
97 | }
98 | }
--------------------------------------------------------------------------------
/fallery/src/test/java/ir/mehdiyari/fallery/buckets/ui/bucketList/BucketListViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package ir.mehdiyari.fallery.buckets.ui.bucketList
2 |
3 | import android.util.Log
4 | import io.mockk.*
5 | import ir.mehdiyari.fallery.buckets.bucketList.BucketListViewModel
6 | import ir.mehdiyari.fallery.buckets.bucketList.LoadingViewState
7 | import ir.mehdiyari.fallery.models.BucketType
8 | import ir.mehdiyari.fallery.models.MediaBucket
9 | import ir.mehdiyari.fallery.repo.AbstractMediaBucketProvider
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.flow.FlowCollector
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.test.*
15 | import org.junit.jupiter.api.AfterEach
16 | import org.junit.jupiter.api.BeforeEach
17 | import org.junit.jupiter.api.Test
18 |
19 | @OptIn(ExperimentalCoroutinesApi::class)
20 | internal class BucketListViewModelTest {
21 |
22 | private val abstractMediaBucketProvider by lazy { mockk() }
23 |
24 | private val bucketViewModel by lazy {
25 | spyk(
26 | BucketListViewModel(
27 | false,
28 | abstractMediaBucketProvider,
29 | BucketType.VIDEO_PHOTO_BUCKETS,
30 | testCoroutineDispatcher,
31 | mockk(relaxed = true)
32 | )
33 | )
34 | }
35 |
36 | private val mockViewStateCollector by lazy { spyk>() }
37 | private val bucketsMutableStateFlowCollector by lazy { spyk>>() }
38 | private val allMediaCountChangedStateFlowCollector by lazy { spyk>() }
39 |
40 | private val testCoroutineDispatcher = Dispatchers.Unconfined
41 | private val testCoroutineScope by lazy { TestScope() }
42 |
43 | @BeforeEach
44 | fun before() {
45 | Dispatchers.setMain(testCoroutineDispatcher)
46 | mockkStatic(Log::class)
47 | every { Log.e(any(), any()) } returns 0
48 | }
49 |
50 | @Test
51 | fun `Given refresh=true - when app has not access to external storage - then notify showLoading-hideLoading-ErrorInFetchingBuckets`() = runBlockingTest {
52 | every { bucketViewModel.bucketsStateFlow.value } returns generateBucketsList()
53 | testCoroutineScope.launch { bucketViewModel.loadingViewStateFlow.collect(mockViewStateCollector) }
54 | coEvery { abstractMediaBucketProvider.getMediaBuckets(any()) } throws SecurityException("cant access to external storage")
55 | bucketViewModel.getBuckets(refresh = false)
56 | coVerify(exactly = 1) { mockViewStateCollector.emit(LoadingViewState.ShowLoading) }
57 | coVerify(exactly = 1) { mockViewStateCollector.emit(LoadingViewState.HideLoading) }
58 | coVerify(exactly = 1) { mockViewStateCollector.emit(LoadingViewState.Error) }
59 | }
60 |
61 | @Test
62 | fun `Given refresh=false - when bucket lists is empty - then get bucket lists`() = runBlockingTest {
63 | val generatedList = generateBucketsList()
64 | testCoroutineScope.launch { bucketViewModel.loadingViewStateFlow.collect(mockViewStateCollector) }
65 | testCoroutineScope.launch { bucketViewModel.bucketsStateFlow.collect(bucketsMutableStateFlowCollector) }
66 | testCoroutineScope.launch { bucketViewModel.allMediaCountChanged.collect(allMediaCountChangedStateFlowCollector) }
67 | coEvery { abstractMediaBucketProvider.getMediaBuckets(any()) } returns generatedList
68 | bucketViewModel.getBuckets(refresh = false)
69 | coVerify(exactly = 1) { mockViewStateCollector.emit(LoadingViewState.ShowLoading) }
70 | coVerify(exactly = 1) { mockViewStateCollector.emit(LoadingViewState.HideLoading) }
71 | coVerify(exactly = 1) { bucketsMutableStateFlowCollector.emit(generatedList) }
72 | coVerify(exactly = 1) { allMediaCountChangedStateFlowCollector.emit(generatedList[0].mediaCount) }
73 | }
74 |
75 | @AfterEach
76 | fun after() {
77 | Dispatchers.resetMain()
78 | }
79 |
80 | private fun generateBucketsList(): List = mutableListOf().apply {
81 | repeat(20) {
82 | add(
83 | MediaBucket(it.toLong(), "storage/$it/emulated/x", "Batman Folder $it", "batman.jpg", (it + 1) * 100)
84 | )
85 | }
86 |
87 | add(0, this[0].copy(displayName = "All Medias", mediaCount = this.sumOf { it.mediaCount }))
88 | }
89 | }
--------------------------------------------------------------------------------
/fallery/src/main/java/ir/mehdiyari/fallery/main/fallery/FalleryOptions.kt:
--------------------------------------------------------------------------------
1 | package ir.mehdiyari.fallery.main.fallery
2 |
3 | import android.content.pm.ActivityInfo
4 | import android.graphics.Color
5 | import androidx.annotation.DrawableRes
6 | import androidx.annotation.LayoutRes
7 | import androidx.annotation.StringRes
8 | import androidx.annotation.StyleRes
9 | import androidx.viewpager2.widget.ViewPager2
10 | import ir.mehdiyari.fallery.R
11 | import ir.mehdiyari.fallery.imageLoader.FalleryImageLoader
12 | import ir.mehdiyari.fallery.models.BucketType
13 | import ir.mehdiyari.fallery.repo.AbstractBucketContentProvider
14 | import ir.mehdiyari.fallery.repo.AbstractMediaBucketProvider
15 | import ir.mehdiyari.fallery.utils.EnumType
16 |
17 | data class FalleryOptions(
18 | val mediaTypeFilter: BucketType = BucketType.VIDEO_PHOTO_BUCKETS,
19 | val maxSelectableMedia: Int = UNLIMITED_SELECT,
20 | val cameraEnabledOptions: CameraEnabledOptions,
21 | val captionEnabledOptions: CaptionEnabledOptions,
22 | val mediaCountEnabled: Boolean,
23 | val imageLoader: FalleryImageLoader?,
24 | val bucketProviderAbstract: AbstractMediaBucketProvider?,
25 | val abstractBucketContentProvider: AbstractBucketContentProvider?,
26 | @StyleRes val themeResId: Int,
27 | val orientationMode: Int,
28 | var bucketRecyclerViewItemMode: BucketRecyclerViewItemMode,
29 | val bucketItemModeToggleEnabled: Boolean,
30 | var mediaObserverEnabled: Boolean,
31 | @StringRes var toolbarTitle: Int = R.string.fallery_toolbar_title,
32 | val mediaPreviewPageTransformer: ViewPager2.PageTransformer? = null,
33 | val mediaPreviewScrollOrientation: Int = ViewPager2.ORIENTATION_HORIZONTAL,
34 | val selectedMediaToggleBackgroundColor: Int = Color.parseColor("#A11183"),
35 | val onVideoPlayClick: ((path: String) -> Unit)? = null,
36 | val grantExternalStoragePermission: Boolean = true,
37 | @Deprecated("Deprecated, This flag is removed in next release.") val grantSharedStorePermission: Boolean = false,
38 | val falleryBucketsSpanCountMode: FalleryBucketsSpanCountMode = FalleryBucketsSpanCountMode.Automatic
39 | ) {
40 | constructor(falleryImageLoader: FalleryImageLoader?) : this(
41 | mediaTypeFilter = BucketType.VIDEO_PHOTO_BUCKETS,
42 | maxSelectableMedia = UNLIMITED_SELECT,
43 | cameraEnabledOptions = CameraEnabledOptions(),
44 | captionEnabledOptions = CaptionEnabledOptions(),
45 | mediaCountEnabled = true,
46 | imageLoader = falleryImageLoader,
47 | bucketProviderAbstract = null,
48 | abstractBucketContentProvider = null,
49 | themeResId = R.style.Fallery_Light,
50 | orientationMode = ActivityInfo.SCREEN_ORIENTATION_USER,
51 | bucketRecyclerViewItemMode = BucketRecyclerViewItemMode.GridStyle,
52 | bucketItemModeToggleEnabled = true,
53 | mediaObserverEnabled = false,
54 | toolbarTitle = R.string.fallery_toolbar_title,
55 | mediaPreviewPageTransformer = null,
56 | mediaPreviewScrollOrientation = ViewPager2.ORIENTATION_HORIZONTAL,
57 | selectedMediaToggleBackgroundColor = Color.parseColor("#A11183"),
58 | onVideoPlayClick = null,
59 | grantExternalStoragePermission = true,
60 | grantSharedStorePermission = false,
61 | falleryBucketsSpanCountMode = FalleryBucketsSpanCountMode.Automatic
62 | )
63 | }
64 |
65 | const val UNLIMITED_SELECT = 0
66 |
67 | data class CameraEnabledOptions(
68 | val enabled: Boolean = false,
69 | val fileProviderAuthority: String? = null,
70 | val directory: String? = null
71 | ) {
72 | constructor(enabled: Boolean, fileProviderAuthority: String) : this(
73 | enabled,
74 | fileProviderAuthority,
75 | null
76 | )
77 | }
78 |
79 | data class CaptionEnabledOptions(
80 | val enabled: Boolean = false,
81 | @DrawableRes val sendIcon: Int = R.drawable.fallery_icon_send,
82 | @LayoutRes val editTextLayoutResId: Int = R.layout.caption_edit_text_layout
83 | ) {
84 | constructor(enabled: Boolean) : this(
85 | enabled,
86 | R.drawable.fallery_icon_send,
87 | R.layout.caption_edit_text_layout
88 | )
89 | }
90 |
91 |
92 | enum class BucketRecyclerViewItemMode constructor(override var value: Int) : EnumType {
93 | GridStyle(R.layout.grid_bucket_item_view),
94 | LinearStyle(R.layout.linear_bucket_item_view)
95 | }
96 |
97 | enum class FalleryBucketsSpanCountMode {
98 | Automatic, // based on device width
99 | UserZoomInOrZoomOut // based on device width but users can changed span count by zoomIn or ZoomOut
100 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.4.1"
3 | androidJunit5 = "1.10.2.0"
4 | androidx-lifecycle-runtime-ktx = "2.8.3"
5 | aspectRatioImageView = "1.0.1"
6 | gradle = "8.5.2"
7 | kspPlugin = "2.0.21-1.0.25"
8 | constraintlayout = "2.1.4"
9 | core-testing = "1.1.1"
10 | glide = "4.16.0"
11 | junit-jupiter-api = "5.11.3"
12 | kotlin = "2.0.10"
13 | core-ktx = "1.15.0"
14 | junit = "4.13.2"
15 | androidx-test-ext-junit = "1.2.1"
16 | espresso-core = "3.5.1"
17 | appcompat = "1.7.0"
18 | kotlin-stdlib-jdk7 = "2.0.0"
19 | kotlinx-coroutines-android = "1.9.0"
20 | leakcanary-android = "2.14"
21 | lifecycle-extensions = "2.2.0"
22 | material = "1.11.0"
23 | mockk = "1.13.11"
24 | lifecycle-runtime-ktx = "2.8.3"
25 | multidex = "1.0.3"
26 | photoView = "2.3.0"
27 | retrofit = "2.11.0"
28 | roundedimageview = "2.3.0"
29 | truth = "1.4.4"
30 |
31 | [libraries]
32 | android-junit5 = { module = "de.mannodermaus.gradle.plugins:android-junit5", version.ref = "androidJunit5" }
33 | androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
34 | androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycle-extensions" }
35 | androidx-lifecycle-runtime-ktx-v261 = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-ktx" }
36 | aspectRatioImageView = { module = "com.github.mehdiyari:AspectRatioImageView", version.ref = "aspectRatioImageView" }
37 | compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
38 | converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
39 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
40 | core-testing = { module = "android.arch.core:core-testing", version.ref = "core-testing" }
41 | glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
42 | gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
43 | junit = { group = "junit", name = "junit", version.ref = "junit" }
44 | androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
45 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
46 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
47 | junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter-api" }
48 | junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter-api" }
49 | junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-jupiter-api" }
50 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
51 | kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin-stdlib-jdk7" }
52 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
53 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-android" }
54 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-android" }
55 | leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary-android" }
56 | leakcanary-object-watcher-android = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android", version.ref = "leakcanary-android" }
57 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
58 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
59 | mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
60 | multidex = { module = "com.android.support:multidex", version.ref = "multidex" }
61 | photoView = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoView" }
62 | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
63 | roundedimageview = { module = "com.makeramen:roundedimageview", version.ref = "roundedimageview" }
64 | truth = { module = "com.google.truth:truth", version.ref = "truth" }
65 |
66 | [plugins]
67 | androidApplication = { id = "com.android.application", version.ref = "agp" }
68 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
69 | android-library = { id = "com.android.library", version.ref = "agp" }
70 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
71 | kspPlugin = { id = "com.google.devtools.ksp", version.ref = "kspPlugin" }
--------------------------------------------------------------------------------
/fallery/src/main/res/layout/fragment_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
25 |
26 |
29 |
30 |
44 |
45 |
46 |
62 |
63 |
71 |
72 |
80 |
81 |
84 |
85 |
90 |
91 |
92 |
93 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/fallery/src/main/java/ir/mehdiyari/fallery/buckets/bucketList/adapter/BucketListAdapter.kt:
--------------------------------------------------------------------------------
1 | package ir.mehdiyari.fallery.buckets.bucketList.adapter
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.LayoutRes
8 | import androidx.appcompat.widget.AppCompatImageView
9 | import androidx.appcompat.widget.AppCompatTextView
10 | import androidx.constraintlayout.widget.ConstraintLayout
11 | import androidx.recyclerview.widget.ListAdapter
12 | import androidx.recyclerview.widget.RecyclerView
13 | import ir.mehdiyari.fallery.R
14 | import ir.mehdiyari.fallery.imageLoader.FalleryImageLoader
15 | import ir.mehdiyari.fallery.imageLoader.PhotoDiminution
16 | import ir.mehdiyari.fallery.models.MediaBucket
17 | import ir.mehdiyari.fallery.utils.getPhotoDimension
18 | import ir.mehdiyari.fallery.utils.toReadableCount
19 |
20 | internal class BucketListAdapter(
21 | mediaBucketDiffCallback: MediaBucketDiffCallback,
22 | private val imageLoader: FalleryImageLoader,
23 | private val placeHolderColor: Int
24 | ) : ListAdapter(mediaBucketDiffCallback) {
25 |
26 | var onBucketClick: ((bucketId: Long) -> (Unit))? = null
27 | var getImageViewWidth: (() -> Int)? = null
28 | private var imageViewSizeForScalingImages = 0
29 |
30 | init {
31 | setHasStableIds(true)
32 | }
33 |
34 | @LayoutRes
35 | var viewHolderId: Int = R.layout.grid_bucket_item_view
36 |
37 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BucketViewHolder =
38 | BucketViewHolder(LayoutInflater.from(parent.context).inflate(viewType, parent, false))
39 |
40 | override fun onBindViewHolder(holder: BucketViewHolder, position: Int) = holder.bind()
41 |
42 | override fun getItemId(position: Int): Long = currentList[position].id
43 |
44 | private fun getPhotoDimensionBasedOnRecyclerViewBucketImageContainerSize(): PhotoDiminution {
45 | if (imageViewSizeForScalingImages == 0)
46 | imageViewSizeForScalingImages = getImageViewWidth?.invoke() ?: 0
47 |
48 | return PhotoDiminution(imageViewSizeForScalingImages, imageViewSizeForScalingImages)
49 | }
50 |
51 | inner class BucketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
52 |
53 | private val imageView = itemView.findViewById(R.id.imageViewBucketImage)
54 | private val appCompatTextViewBucketName = itemView.findViewById(R.id.appCompatTextViewBucketName)
55 | private val appCompatTextViewBucketItemCount = itemView.findViewById(R.id.appCompatTextViewBucketItemCount)
56 |
57 | init {
58 | itemView.findViewById(R.id.constraintLayoutBucketItemView).setOnClickListener {
59 | onBucketClick?.invoke(getItemId(adapterPosition))
60 | }
61 | }
62 |
63 | fun bind() {
64 | itemView.apply {
65 | currentList[adapterPosition].also { currentBucket ->
66 | getPhotoDimensionBasedOnRecyclerViewBucketImageContainerSize().also { dimension ->
67 | val originalDim = getPhotoDimension(currentBucket.firstMediaThumbPath)
68 | val widthScale = originalDim.width / dimension.width.toFloat()
69 | val newHeight = originalDim.height / widthScale
70 | if (dimension.isNotSet())
71 | imageLoader.loadPhoto(
72 | context = this.context,
73 | imageView = imageView,
74 | resizeDiminution = getDefaultDimension(this.context),
75 | placeHolderColor = placeHolderColor,
76 | path = currentBucket.firstMediaThumbPath
77 | )
78 | else
79 | imageLoader.loadPhoto(
80 | context = this.context,
81 | imageView = imageView,
82 | resizeDiminution = dimension.copy(height = newHeight.toInt()),
83 | placeHolderColor = placeHolderColor,
84 | path = currentBucket.firstMediaThumbPath
85 | )
86 | }
87 | appCompatTextViewBucketName.text = currentBucket.displayName
88 | appCompatTextViewBucketItemCount.text = setMediaCountBasedOnLayout(currentBucket.mediaCount)
89 | }
90 | }
91 | }
92 |
93 | private fun setMediaCountBasedOnLayout(mediaCount: Int): String = when (viewHolderId) {
94 | R.layout.linear_bucket_item_view -> this@BucketViewHolder.itemView.context.getString(R.string.media_count, mediaCount)
95 | else -> mediaCount.toReadableCount()
96 | }
97 |
98 | private fun getDefaultDimension(context: Context): PhotoDiminution = PhotoDiminution(
99 | context.resources.getDimension(R.dimen.fallery_bucket_max_width).toInt(), 0
100 | )
101 | }
102 |
103 | override fun getItemViewType(position: Int): Int = viewHolderId
104 | }
--------------------------------------------------------------------------------
/fallery/src/main/java/ir/mehdiyari/fallery/buckets/bucketContent/BucketContentViewModel.kt:
--------------------------------------------------------------------------------
1 | package ir.mehdiyari.fallery.buckets.bucketContent
2 |
3 | import androidx.lifecycle.LiveData
4 | import ir.mehdiyari.fallery.buckets.bucketList.LoadingViewState
5 | import ir.mehdiyari.fallery.models.BucketType
6 | import ir.mehdiyari.fallery.models.Media
7 | import ir.mehdiyari.fallery.repo.AbstractBucketContentProvider
8 | import ir.mehdiyari.fallery.utils.BUCKET_CONTENT_DEFAULT_SPAN_COUNT
9 | import ir.mehdiyari.fallery.utils.BaseViewModel
10 | import ir.mehdiyari.fallery.utils.SingleLiveEvent
11 | import kotlinx.coroutines.CoroutineDispatcher
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.flow.StateFlow
15 | import kotlinx.coroutines.flow.catch
16 | import kotlinx.coroutines.launch
17 | import java.util.concurrent.atomic.AtomicBoolean
18 |
19 | internal class BucketContentViewModel(
20 | private val abstractBucketContentProvider: AbstractBucketContentProvider,
21 | private val bucketType: BucketType,
22 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
23 | ) : BaseViewModel() {
24 |
25 | private val medias = MutableStateFlow>(listOf())
26 | val mediaList: StateFlow> = medias
27 |
28 | private val _showPreviewFragmentLiveData = SingleLiveEvent()
29 | val showPreviewFragmentLiveData: LiveData = _showPreviewFragmentLiveData
30 |
31 | private val loadingMutableStateFlow = MutableStateFlow(null)
32 | val loadingViewStateFlow: StateFlow = loadingMutableStateFlow
33 |
34 | private val _spanCountStateFlow = MutableStateFlow(null)
35 | val spanCountStateFlow: StateFlow = _spanCountStateFlow
36 |
37 | fun getMedias(bucketId: Long, refresh: Boolean = false) {
38 | if (!refresh && mediaList.value.isNotEmpty()) return
39 | val clearList = AtomicBoolean(refresh)
40 | loadingMutableStateFlow.value = LoadingViewState.ShowLoading
41 | viewModelScope.launch(ioDispatcher) {
42 | abstractBucketContentProvider.getMediasOfBucket(bucketId, bucketType)
43 | .catch {
44 | viewModelScope.launch(Dispatchers.Main) {
45 | loadingMutableStateFlow.value = LoadingViewState.Error
46 | }
47 | }
48 | .collect {
49 | if (medias.value.isEmpty()) {
50 | viewModelScope.launch(Dispatchers.Main) {
51 | loadingMutableStateFlow.value = LoadingViewState.HideLoading
52 | }
53 | }
54 |
55 | if (clearList.get()) {
56 | clearList.compareAndSet(true, false)
57 | medias.value = it
58 | } else {
59 | medias.value = mediaList.value.toMutableList().apply { addAll(it) }.toList()
60 | }
61 | }
62 | }
63 | }
64 |
65 | fun showPreviewFragment(path: String) {
66 | _showPreviewFragmentLiveData.value = path
67 | }
68 |
69 | fun getIndexOfPath(path: String): Int =
70 | mediaList.value.indexOfFirst { it.getMediaPath() == path.trim() }
71 |
72 | fun getMediaPathByPosition(position: Int): String? =
73 | mediaList.value.getOrNull(position)?.getMediaPath()
74 |
75 | fun retry(bucketId: Long) {
76 | getMedias(bucketId, true)
77 | }
78 |
79 | fun changeSpanCountBasedOnUserTouch(
80 | zoomIn: Boolean,
81 | maxSpanCount: Int,
82 | minSpanCount: Int,
83 | spanCount: Int?,
84 | isPortrait: Boolean
85 | ) {
86 | val span = spanCount ?: BUCKET_CONTENT_DEFAULT_SPAN_COUNT
87 | val newSpan = if (!zoomIn && span < maxSpanCount) {
88 | span + 1
89 | } else if (zoomIn && span > minSpanCount) {
90 | span - 1
91 | } else {
92 | span
93 | }
94 |
95 | viewModelScope.launch {
96 | _spanCountStateFlow.value.also { spanCountState ->
97 | if (spanCountState != null) {
98 | _spanCountStateFlow.emit(
99 | if (isPortrait)
100 | spanCountState.copy(portraitSpanCount = newSpan)
101 | else
102 | spanCountState.copy(landScapeSpanCount = newSpan)
103 | )
104 | } else {
105 | _spanCountStateFlow.emit(
106 | if (isPortrait) {
107 | BucketContentSpanCount(
108 | portraitSpanCount = newSpan,
109 | landScapeSpanCount = newSpan
110 | )
111 | } else {
112 | BucketContentSpanCount(
113 | portraitSpanCount = BUCKET_CONTENT_DEFAULT_SPAN_COUNT,
114 | landScapeSpanCount = newSpan
115 | )
116 | }
117 | )
118 | }
119 | }
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/fallery/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
28 |
29 |
30 |
52 |
53 |
58 |
59 |
64 |
65 |
69 |
70 |
85 |
86 |
89 |
90 |
93 |
94 |
97 |
98 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------