├── .gitignore ├── BottomDialogFilePicker ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── mahdiasd │ │ └── bottomdialogfilepicker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── mahdiasd │ │ │ └── bottomdialogfilepicker │ │ │ ├── FilePickerDialog.kt │ │ │ ├── Items.kt │ │ │ ├── PickerConfig.kt │ │ │ ├── PickerFile.kt │ │ │ └── PickerUtils.kt │ └── res │ │ ├── drawable │ │ ├── mahdiasd_ic_audio.xml │ │ ├── mahdiasd_ic_camera.xml │ │ ├── mahdiasd_ic_camera_shutter.xml │ │ ├── mahdiasd_ic_change_camera.xml │ │ ├── mahdiasd_ic_checkmark.xml │ │ ├── mahdiasd_ic_close.xml │ │ ├── mahdiasd_ic_document.xml │ │ ├── mahdiasd_ic_done.xml │ │ ├── mahdiasd_ic_file.xml │ │ ├── mahdiasd_ic_gallery.xml │ │ ├── mahdiasd_ic_image.xml │ │ ├── mahdiasd_ic_music.xml │ │ ├── mahdiasd_ic_play.xml │ │ ├── mahdiasd_ic_search.xml │ │ ├── mahdiasd_ic_send.xml │ │ ├── mahdiasd_ic_storage.xml │ │ ├── mahdiasd_ic_tick.xml │ │ └── mahdiasd_ic_video.xml │ │ └── xml │ │ └── provider_paths.xml │ └── test │ └── java │ └── mahdiasd │ └── bottomdialogfilepicker │ └── ExampleUnitTest.kt ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── mahdiasd │ │ └── bottomdialogfilepicker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── mahdiasd │ │ │ └── bottomdialogfilepicker │ │ │ ├── MainActivity.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── mahdiasd │ └── bottomdialogfilepicker │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── key.jks └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlinAndroid) 4 | id("maven-publish") 5 | } 6 | 7 | android { 8 | compileSdk = 33 9 | namespace = "mahdiasd.bottomdialogfilepicker" 10 | 11 | defaultConfig { 12 | aarMetadata { 13 | minCompileSdk = 29 14 | } 15 | minSdk = 21 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | consumerProguardFiles("consumer-rules.pro") 19 | } 20 | 21 | buildTypes { 22 | release { 23 | isMinifyEnabled = false 24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 25 | } 26 | } 27 | 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | targetCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = "1.8" 34 | } 35 | composeOptions { 36 | kotlinCompilerExtensionVersion = "1.4.3" 37 | } 38 | buildFeatures { 39 | compose = true 40 | } 41 | 42 | publishing { 43 | singleVariant("release") { 44 | withSourcesJar() 45 | withJavadocJar() 46 | } 47 | } 48 | 49 | } 50 | 51 | 52 | dependencies { 53 | 54 | implementation(libs.core.ktx) 55 | implementation(libs.lifecycle.runtime.ktx) 56 | implementation(libs.activity.compose) 57 | implementation(platform(libs.compose.bom)) 58 | 59 | implementation(libs.ui) 60 | implementation(libs.ui.graphics) 61 | implementation(libs.ui.tooling.preview) 62 | implementation(libs.material3) 63 | implementation(libs.constraintlayout) 64 | 65 | implementation(libs.kotlinx.collections.immutable) 66 | implementation(libs.coil.compose) 67 | implementation(libs.coil.video) 68 | 69 | implementation(libs.accompanist.permissions) 70 | 71 | implementation(libs.handle.path.oz) 72 | 73 | testImplementation(libs.junit) 74 | androidTestImplementation(libs.androidx.test.ext.junit) 75 | androidTestImplementation(libs.espresso.core) 76 | } 77 | 78 | publishing { 79 | publications { 80 | register("release") { 81 | groupId = "mahdiasd.bottomdialogfilepicker" 82 | artifactId = "compose_bottom_dialog_file_picker" 83 | version = "1.0.0" 84 | 85 | afterEvaluate { 86 | from(components["release"]) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/BottomDialogFilePicker/consumer-rules.pro -------------------------------------------------------------------------------- /BottomDialogFilePicker/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/androidTest/java/mahdiasd/bottomdialogfilepicker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("mahdiasd.bottomdialogfilepicker.test", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 18 | 19 | 20 | 21 | 24 | 29 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/java/mahdiasd/bottomdialogfilepicker/FilePickerDialog.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import android.content.res.Configuration 4 | import android.graphics.Bitmap 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import androidx.activity.compose.rememberLauncherForActivityResult 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.compose.animation.core.animateFloatAsState 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.border 12 | import androidx.compose.foundation.clickable 13 | import androidx.compose.foundation.clipScrollableContainer 14 | import androidx.compose.foundation.gestures.Orientation 15 | import androidx.compose.foundation.layout.Arrangement 16 | import androidx.compose.foundation.layout.Box 17 | import androidx.compose.foundation.layout.Column 18 | import androidx.compose.foundation.layout.PaddingValues 19 | import androidx.compose.foundation.layout.Spacer 20 | import androidx.compose.foundation.layout.aspectRatio 21 | import androidx.compose.foundation.layout.defaultMinSize 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.height 24 | import androidx.compose.foundation.layout.offset 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.foundation.layout.requiredHeight 27 | import androidx.compose.foundation.layout.size 28 | import androidx.compose.foundation.layout.sizeIn 29 | import androidx.compose.foundation.layout.wrapContentSize 30 | import androidx.compose.foundation.lazy.LazyColumn 31 | import androidx.compose.foundation.lazy.LazyRow 32 | import androidx.compose.foundation.lazy.grid.GridCells 33 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 34 | import androidx.compose.foundation.lazy.grid.items 35 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 36 | import androidx.compose.foundation.lazy.rememberLazyListState 37 | import androidx.compose.foundation.shape.CircleShape 38 | import androidx.compose.foundation.shape.RoundedCornerShape 39 | import androidx.compose.foundation.text.KeyboardOptions 40 | import androidx.compose.material3.ButtonDefaults 41 | import androidx.compose.material3.ExperimentalMaterial3Api 42 | import androidx.compose.material3.Icon 43 | import androidx.compose.material3.ModalBottomSheet 44 | import androidx.compose.material3.SheetState 45 | import androidx.compose.material3.Text 46 | import androidx.compose.material3.TextField 47 | import androidx.compose.material3.TextFieldDefaults 48 | import androidx.compose.material3.rememberModalBottomSheetState 49 | import androidx.compose.runtime.Composable 50 | import androidx.compose.runtime.LaunchedEffect 51 | import androidx.compose.runtime.derivedStateOf 52 | import androidx.compose.runtime.getValue 53 | import androidx.compose.runtime.mutableStateListOf 54 | import androidx.compose.runtime.mutableStateOf 55 | import androidx.compose.runtime.remember 56 | import androidx.compose.runtime.rememberCoroutineScope 57 | import androidx.compose.runtime.setValue 58 | import androidx.compose.ui.Alignment 59 | import androidx.compose.ui.Modifier 60 | import androidx.compose.ui.draw.alpha 61 | import androidx.compose.ui.draw.drawBehind 62 | import androidx.compose.ui.draw.scale 63 | import androidx.compose.ui.draw.shadow 64 | import androidx.compose.ui.geometry.Offset 65 | import androidx.compose.ui.graphics.Color 66 | import androidx.compose.ui.layout.onGloballyPositioned 67 | import androidx.compose.ui.platform.LocalConfiguration 68 | import androidx.compose.ui.platform.LocalContext 69 | import androidx.compose.ui.platform.LocalDensity 70 | import androidx.compose.ui.res.painterResource 71 | import androidx.compose.ui.text.font.FontWeight 72 | import androidx.compose.ui.text.input.ImeAction 73 | import androidx.compose.ui.text.input.KeyboardType 74 | import androidx.compose.ui.text.style.TextAlign 75 | import androidx.compose.ui.unit.IntOffset 76 | import androidx.compose.ui.unit.dp 77 | import androidx.compose.ui.unit.sp 78 | import br.com.onimur.handlepathoz.HandlePathOz 79 | import br.com.onimur.handlepathoz.HandlePathOzListener 80 | import br.com.onimur.handlepathoz.model.PathOz 81 | import coil.ImageLoader 82 | import coil.decode.VideoFrameDecoder 83 | import coil.request.CachePolicy 84 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 85 | import kotlinx.collections.immutable.ImmutableList 86 | import kotlinx.collections.immutable.toImmutableList 87 | import kotlinx.coroutines.FlowPreview 88 | import kotlinx.coroutines.delay 89 | import kotlinx.coroutines.launch 90 | import mahdiasd.bottomdialogfilepicker.PickerUtils.getAudio 91 | import mahdiasd.bottomdialogfilepicker.PickerUtils.getImage 92 | import mahdiasd.bottomdialogfilepicker.PickerUtils.getVideo 93 | import mahdiasd.bottomdialogfilepicker.PickerUtils.permissionState 94 | import mahdiasd.bottomdialogfilepicker.PickerUtils.toDp 95 | import mahdiasd.bottomdialogfilepicker.PickerUtils.toPx 96 | import java.io.File 97 | 98 | @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) 99 | @Composable 100 | fun FilePickerDialog( 101 | config: PickerConfig, 102 | modes: List = PickerUtils.allModes, 103 | onDismissDialog: () -> Unit, 104 | selectedFiles: (List) -> Unit, 105 | ) { 106 | 107 | val permissionsState = permissionState(config.enableCamera) 108 | if (!permissionsState.allPermissionsGranted) { 109 | LaunchedEffect(key1 = true, block = { 110 | permissionsState.launchMultiplePermissionRequest() 111 | }) 112 | } else { 113 | 114 | val c = LocalContext.current 115 | val images = remember { mutableStateListOf() }.apply { 116 | addAll(getImage(c, config.enableCamera)) 117 | } 118 | val audios = remember { mutableStateListOf() }.apply { 119 | addAll(getAudio(c)) 120 | } 121 | val videos = remember { mutableStateListOf() }.apply { 122 | addAll(getVideo(c)) 123 | } 124 | 125 | val currentType = remember { mutableStateOf(config.currentType) } 126 | var searchText by remember { mutableStateOf("") } 127 | val coroutineScope = rememberCoroutineScope() 128 | 129 | val files by remember { 130 | derivedStateOf { 131 | when (currentType.value) { 132 | PickerType.Image -> images 133 | PickerType.Video -> videos 134 | PickerType.File -> listOf() 135 | PickerType.Audio -> { 136 | if (searchText.isEmpty()) 137 | audios 138 | else 139 | audios.filter { it.path.lowercase().contains(searchText.lowercase()) } 140 | } 141 | }.distinctBy { it.path } 142 | } 143 | } 144 | 145 | val bottomSheetState = rememberModalBottomSheetState() 146 | 147 | val selectedFilesCount = remember { mutableStateOf(0) } 148 | 149 | 150 | BottomSheetDialog( 151 | pickerModes = modes.toImmutableList(), 152 | bottomSheetState = bottomSheetState, 153 | config = config, 154 | currentType = currentType.value, 155 | files = files.toImmutableList(), 156 | selectedCount = selectedFilesCount.value, 157 | itemTypeClick = { currentType.value = it }, 158 | onDismissDialog = onDismissDialog, 159 | searchText = searchText, 160 | onSearchChange = { 161 | searchText = it 162 | }, 163 | onChangeSelect = { pickerFile -> 164 | when (currentType.value) { 165 | PickerType.Image -> { 166 | val index = images.indexOfFirst { it.path == pickerFile.path }.takeIf { it >= 0 } ?: return@BottomSheetDialog 167 | images[index] = images[index].copy(selected = !pickerFile.selected) 168 | } 169 | 170 | PickerType.Video -> { 171 | val index = videos.indexOfFirst { it.path == pickerFile.path }.takeIf { it >= 0 } ?: return@BottomSheetDialog 172 | videos[index] = videos[index].copy(selected = !pickerFile.selected) 173 | } 174 | 175 | PickerType.File -> { 176 | 177 | } 178 | 179 | PickerType.Audio -> { 180 | val index = audios.indexOfFirst { it.path == pickerFile.path }.takeIf { it >= 0 } ?: return@BottomSheetDialog 181 | audios[index] = audios[index].copy(selected = !pickerFile.selected) 182 | } 183 | } 184 | 185 | val total = (images + audios + videos).filter { it.selected } 186 | 187 | if (total.size > config.maxSelection) { 188 | total.firstOrNull()?.let { 189 | images.indexOfFirst { it == total.first() }.takeIf { it > 0 }?.apply { images[this] = images[this].copy(selected = false) } 190 | videos.indexOfFirst { it == total.first() }.takeIf { it > 0 }?.apply { videos[this] = videos[this].copy(selected = false) } 191 | audios.indexOfFirst { it == total.first() }.takeIf { it > 0 }?.apply { audios[this] = audios[this].copy(selected = false) } 192 | } 193 | } 194 | 195 | if (total.size != selectedFilesCount.value) 196 | selectedFilesCount.value = total.size 197 | }, 198 | onDoneClick = { 199 | val s = (images.flatMap { listOf(it) } + audios.flatMap { listOf(it) } + videos.flatMap { listOf(it) }).filter { it.selected }.distinctBy { it.path } 200 | selectedFiles.invoke(s) 201 | onDismissDialog() 202 | coroutineScope.launch { bottomSheetState.hide() } 203 | }, 204 | onCameraPhoto = { pickerFile -> 205 | images.add(1, pickerFile) 206 | val count = (images.flatMap { listOf(it) } + audios.flatMap { listOf(it) } + videos.flatMap { listOf(it) }).filter { it.selected }.size 207 | if (count != selectedFilesCount.value) 208 | selectedFilesCount.value = count 209 | }, 210 | onStoragePicker = { list -> 211 | selectedFiles.invoke(list.distinctBy { it.path }) 212 | onDismissDialog() 213 | coroutineScope.launch { bottomSheetState.hide() } 214 | } 215 | ) 216 | } 217 | 218 | } 219 | 220 | 221 | @OptIn(ExperimentalMaterial3Api::class) 222 | @Composable 223 | fun BottomSheetDialog( 224 | pickerModes: ImmutableList, 225 | bottomSheetState: SheetState, 226 | config: PickerConfig, 227 | currentType: PickerType, 228 | files: ImmutableList, 229 | selectedCount: Int, 230 | itemTypeClick: (PickerType) -> Unit, 231 | onChangeSelect: (PickerFile) -> Unit, 232 | searchText: String = "", 233 | onSearchChange: (String) -> Unit, 234 | onDismissDialog: () -> Unit, 235 | onDoneClick: () -> Unit, 236 | onCameraPhoto: (PickerFile) -> Unit, 237 | onStoragePicker: (List) -> Unit, 238 | isLandscape: Boolean = LocalConfiguration.current.orientation != Configuration.ORIENTATION_PORTRAIT 239 | 240 | ) { 241 | val density = LocalDensity.current.density 242 | var modalHeight by remember { mutableStateOf(0) } 243 | var modalWidth by remember { mutableStateOf(0) } 244 | var footerHeight by remember { mutableStateOf(0) } 245 | 246 | val bottomPadding = ButtonDefaults.MinHeight.toPx() 247 | 248 | val context = LocalContext.current 249 | val mode = pickerModes.find { it.pickerType == currentType } ?: PickerMode(currentType) 250 | 251 | val imageLoader = remember { 252 | ImageLoader.Builder(context) 253 | .components { add(VideoFrameDecoder.Factory()) } 254 | .memoryCachePolicy(CachePolicy.ENABLED) 255 | .diskCachePolicy(CachePolicy.ENABLED) 256 | .crossfade(true) 257 | .build() 258 | } 259 | 260 | var horizontalArrangement by remember { mutableStateOf(Arrangement.SpaceEvenly) } 261 | LaunchedEffect(key1 = selectedCount, block = { 262 | horizontalArrangement = if (selectedCount > 0 && !isLandscape) Arrangement.spacedBy(16.dp) else Arrangement.SpaceEvenly 263 | }) 264 | 265 | val doneAlpha = animateFloatAsState(targetValue = if (selectedCount > 0) 1f else 0f, label = "doneAlphaAnimation") 266 | 267 | 268 | 269 | ModalBottomSheet( 270 | modifier = Modifier 271 | .defaultMinSize(minHeight = modalHeight.toDp()) 272 | .onGloballyPositioned { 273 | modalHeight = it.size.height 274 | modalWidth = it.size.width 275 | }, 276 | sheetState = bottomSheetState, 277 | containerColor = config.containerColor, 278 | scrimColor = config.scrimColor ?: Color.Gray, 279 | onDismissRequest = onDismissDialog 280 | ) { 281 | 282 | Box( 283 | modifier = Modifier 284 | .fillMaxWidth() 285 | .padding(0.dp) 286 | ) { 287 | 288 | when (currentType) { 289 | PickerType.Image, PickerType.Video -> { 290 | if (files.isEmpty()) 291 | Text(modifier = Modifier.fillMaxWidth(), text = config.noItemMessage, style = config.noItemStyle) 292 | else 293 | ImageAndVideoScreen(config, imageLoader, modalHeight, files, mode, onChangeSelect, onCameraPhoto) 294 | } 295 | 296 | PickerType.File -> { 297 | FileScreen(config, onStoragePicker = onStoragePicker) 298 | } 299 | 300 | PickerType.Audio -> { 301 | if (files.isEmpty()) 302 | Text(modifier = Modifier.fillMaxWidth(), text = config.noItemMessage, style = config.noItemStyle) 303 | else 304 | AudioScreen(config, modalHeight, files, onChangeSelect, searchText, onSearchChange) 305 | } 306 | } 307 | 308 | LazyRow( 309 | modifier = Modifier 310 | .fillMaxWidth() 311 | .requiredHeight(200.dp) 312 | .offset { 313 | IntOffset( 314 | 0, 315 | (modalHeight - bottomSheetState.requireOffset() - footerHeight).toInt() 316 | ) 317 | } 318 | .fillMaxWidth() 319 | .padding(top = 12.dp, end = 0.dp, start = 0.dp, bottom = 42.dp) 320 | .onGloballyPositioned { 321 | footerHeight = (((it.size.height * 1)) + bottomPadding + 0).toInt() 322 | } 323 | .drawBehind { 324 | drawLine( 325 | color = Color.DarkGray, 326 | start = Offset(0f, 0f), 327 | end = Offset(size.width, 0f), 328 | strokeWidth = 5f 329 | ) 330 | } 331 | .shadow(6.dp, RoundedCornerShape(1.dp), spotColor = Color.Gray) 332 | .background(color = config.containerColor, RoundedCornerShape(1.dp)), 333 | horizontalArrangement = horizontalArrangement, 334 | contentPadding = PaddingValues(16.dp) 335 | ) { 336 | items(pickerModes.size, key = { pickerModes[it].title }) { index -> 337 | val item = pickerModes[index] 338 | Column( 339 | modifier = Modifier 340 | .fillMaxWidth(), 341 | horizontalAlignment = Alignment.CenterHorizontally, 342 | verticalArrangement = Arrangement.SpaceAround 343 | ) { 344 | Icon( 345 | painter = painterResource(id = item.icon), 346 | contentDescription = "${item.pickerType.name} icon", 347 | Modifier 348 | .size(item.iconSize) 349 | .padding(4.dp) 350 | .shadow(2.dp, item.shape, spotColor = Color.Gray) 351 | .background(item.shapeColor, item.shape) 352 | .then( 353 | if (currentType == item.pickerType) Modifier 354 | .padding(4.dp) 355 | .border(1.5.dp, color = Color.White.copy(alpha = 0.5f), shape = CircleShape) 356 | else Modifier 357 | ) 358 | .padding(12.dp) 359 | .clickable { itemTypeClick(item.pickerType) }, 360 | tint = item.iconTint, 361 | ) 362 | 363 | Text( 364 | text = item.title, 365 | color = if (currentType == item.pickerType) item.selectedColor else item.itemTextStyle.color, 366 | textAlign = TextAlign.Center, 367 | style = item.itemTextStyle, 368 | modifier = Modifier.clickable { itemTypeClick(item.pickerType) } 369 | ) 370 | } 371 | } 372 | } 373 | 374 | 375 | Box( 376 | modifier = Modifier 377 | .alpha(doneAlpha.value) 378 | .offset { 379 | IntOffset( 380 | modalWidth - (68.dp * density).value.toInt(), 381 | (modalHeight - bottomSheetState.requireOffset() - (1.1 * footerHeight)).toInt() 382 | ) 383 | } 384 | .clickable { onDoneClick() } 385 | .padding(0.dp)) { 386 | 387 | Icon( 388 | modifier = 389 | Modifier 390 | .size(config.doneIconSize) 391 | .shadow(2.dp, CircleShape, spotColor = Color.Gray) 392 | .background(config.doneIconBackground, CircleShape), 393 | tint = config.doneIconTint, 394 | painter = painterResource(id = config.doneIcon), 395 | contentDescription = "done icon" 396 | ) 397 | 398 | if (selectedCount > 0) 399 | Text( 400 | text = "$selectedCount", 401 | modifier = Modifier 402 | .wrapContentSize(unbounded = true) 403 | .border(1.dp, config.containerColor, shape = CircleShape) 404 | .sizeIn(20.dp, 20.dp, 30.dp, 30.dp) 405 | .shadow(2.dp, CircleShape, spotColor = Color.Gray) 406 | .drawBehind { 407 | drawCircle( 408 | color = config.doneBadgeBackgroundColor, 409 | radius = this.size.maxDimension 410 | ) 411 | } 412 | .scale(1f) 413 | .align(Alignment.BottomEnd), 414 | style = config.doneBadgeStyle, 415 | ) 416 | } 417 | } 418 | } 419 | 420 | LaunchedEffect(key1 = isLandscape, block = { 421 | delay(500) 422 | bottomSheetState.expand() 423 | }) 424 | 425 | 426 | } 427 | 428 | @Composable 429 | fun AudioScreen( 430 | config: PickerConfig, 431 | modalHeight: Int, 432 | files: ImmutableList, 433 | onChangeSelect: (PickerFile) -> Unit, 434 | searchText: String, 435 | onSearchChange: (String) -> Unit, 436 | ) { 437 | Column( 438 | modifier = Modifier 439 | .defaultMinSize(minHeight = modalHeight.toDp()) 440 | .fillMaxWidth() 441 | ) { 442 | TextField( 443 | value = searchText, 444 | onValueChange = { 445 | onSearchChange(it) 446 | }, 447 | shape = RoundedCornerShape(16.dp), 448 | singleLine = true, 449 | colors = TextFieldDefaults.colors( 450 | focusedTextColor = config.searchTextStyle.color, 451 | focusedContainerColor = Color.DarkGray, 452 | unfocusedContainerColor = Color.DarkGray, 453 | disabledContainerColor = Color.DarkGray, 454 | focusedIndicatorColor = Color.Transparent, 455 | unfocusedIndicatorColor = Color.Transparent, 456 | disabledIndicatorColor = Color.Transparent, 457 | ), 458 | keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next), 459 | textStyle = androidx.compose.ui.text.TextStyle(fontSize = 12.sp), 460 | placeholder = { 461 | Text( 462 | config.searchTextHint, 463 | modifier = Modifier.fillMaxWidth(), 464 | style = config.searchTextHintStyle, 465 | fontSize = 12.sp, 466 | fontWeight = FontWeight.Normal 467 | ) 468 | }, 469 | modifier = Modifier 470 | .fillMaxWidth() 471 | .padding(horizontal = 16.dp) 472 | ) 473 | 474 | 475 | LazyColumn( 476 | modifier = Modifier.clipScrollableContainer(Orientation.Vertical), 477 | state = rememberLazyListState(), 478 | contentPadding = PaddingValues( 479 | top = 16.dp, 480 | end = 16.dp, 481 | start = 16.dp, 482 | bottom = 150.dp 483 | ), 484 | ) { 485 | items(files.size, key = { files[it].path }) { index -> 486 | MediaAudioItem(files[index]) { 487 | onChangeSelect(files[index]) 488 | } 489 | } 490 | } 491 | } 492 | 493 | } 494 | 495 | @Composable 496 | fun ImageAndVideoScreen( 497 | config: PickerConfig, 498 | imageLoader: ImageLoader, 499 | modalHeight: Int, 500 | files: ImmutableList, 501 | mode: PickerMode, 502 | onChangeSelect: (PickerFile) -> Unit, 503 | onCameraPhoto: (PickerFile) -> Unit, 504 | isLandscape: Boolean = LocalConfiguration.current.orientation != Configuration.ORIENTATION_PORTRAIT 505 | 506 | ) { 507 | val context = LocalContext.current 508 | val mediaListState = rememberLazyGridState() 509 | val cameraPicture = remember { mutableStateOf(null) } 510 | val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { 511 | cameraPicture.value = it 512 | PickerUtils.saveBitmapToStorage(it)?.let { file -> 513 | onCameraPhoto(PickerFile(file.path, file, selected = true)) 514 | } ?: run { 515 | Toast.makeText(context, "Some error while write picture in storage, check permissions...", Toast.LENGTH_LONG).show() 516 | } 517 | } 518 | 519 | LazyVerticalGrid( 520 | columns = GridCells.Adaptive(120.dp), 521 | state = mediaListState, 522 | modifier = Modifier 523 | .fillMaxWidth() 524 | .defaultMinSize(minHeight = modalHeight.toDp()) 525 | .clipScrollableContainer(Orientation.Vertical), 526 | contentPadding = PaddingValues( 527 | top = 16.dp, 528 | end = 16.dp, 529 | start = 16.dp, 530 | bottom = 150.dp 531 | ) 532 | ) { 533 | items(items = files, key = { it.path }) { pickerFile -> 534 | if (pickerFile.path == "show camera") { 535 | Box( 536 | modifier = Modifier 537 | .padding(8.dp) 538 | .background(config.cameraIconBackground, RoundedCornerShape(16.dp)) 539 | .aspectRatio(1f) 540 | ) { 541 | Icon( 542 | painter = painterResource(id = config.cameraIcon), 543 | contentDescription = "camera", 544 | tint = config.cameraIconTint, 545 | modifier = Modifier 546 | .padding(24.dp) 547 | .clickable { 548 | cameraLauncher.launch(null) 549 | } 550 | ) 551 | } 552 | } else { 553 | MediaItem(pickerFile, config, mode, imageLoader = imageLoader) { 554 | onChangeSelect(pickerFile) 555 | } 556 | } 557 | } 558 | } 559 | } 560 | 561 | @OptIn(FlowPreview::class) 562 | @Composable 563 | fun FileScreen(config: PickerConfig, onStoragePicker: (List) -> Unit) { 564 | val context = LocalContext.current 565 | val files = remember { mutableStateOf(listOf()) } 566 | val pathListener = 567 | object : HandlePathOzListener.MultipleUri { 568 | override fun onRequestHandlePathOz(listPathOz: List, tr: Throwable?) { 569 | if (listPathOz.isNotEmpty()) { 570 | listPathOz.map { pathOz -> PickerFile(path = pathOz.path, file = File(pathOz.path), selected = false) } 571 | .let { filePickers -> onStoragePicker.invoke(filePickers.distinctBy { it.path }) } 572 | } 573 | } 574 | } 575 | 576 | val launcher = if (config.maxSelection < 2) { 577 | rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> 578 | if (uri != null) { 579 | files.value = listOf(uri) 580 | HandlePathOz(context, pathListener).getListRealPath(files.value) 581 | } 582 | } 583 | } else { 584 | rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> 585 | files.value = uris 586 | HandlePathOz(context, pathListener).getListRealPath(files.value) 587 | } 588 | } 589 | 590 | Column( 591 | modifier = Modifier 592 | .fillMaxWidth() 593 | .requiredHeight(400.dp) 594 | ) { 595 | config.apply { 596 | FileItem( 597 | icon = storageIcon, 598 | iconBackground = storageIconBackground, 599 | iconTint = storageIconTint, 600 | iconSize = storageIconSize, 601 | title = storageTitle, 602 | description = storageDescription, 603 | titleTextStyle = storageTitleStyle, 604 | descriptionTextStyle = storageDescriptionStyle, 605 | supportRtl = true, 606 | onClicked = { 607 | launcher.launch("*/*") 608 | } 609 | ) 610 | 611 | Spacer( 612 | modifier = Modifier 613 | .height(3.dp) 614 | .background(Color.Gray) 615 | ) 616 | 617 | FileItem( 618 | icon = galleryIcon, 619 | iconBackground = galleryIconBackground, 620 | iconTint = galleryIconTint, 621 | iconSize = galleryIconSize, 622 | title = galleryTitle, 623 | description = galleryDescription, 624 | titleTextStyle = galleryTitleStyle, 625 | descriptionTextStyle = galleryDescriptionStyle, 626 | supportRtl = true, 627 | onClicked = { 628 | launcher.launch("image/*") 629 | } 630 | ) 631 | } 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/java/mahdiasd/bottomdialogfilepicker/Items.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.aspectRatio 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.foundation.layout.width 17 | import androidx.compose.foundation.shape.CircleShape 18 | import androidx.compose.foundation.shape.RoundedCornerShape 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.Done 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.clip 27 | import androidx.compose.ui.draw.shadow 28 | import androidx.compose.ui.graphics.Color 29 | import androidx.compose.ui.layout.ContentScale 30 | import androidx.compose.ui.platform.LocalContext 31 | import androidx.compose.ui.res.painterResource 32 | import androidx.compose.ui.text.TextStyle 33 | import androidx.compose.ui.text.style.TextAlign 34 | import androidx.compose.ui.text.style.TextDirection 35 | import androidx.compose.ui.tooling.preview.Preview 36 | import androidx.compose.ui.unit.Dp 37 | import androidx.compose.ui.unit.dp 38 | import androidx.compose.ui.unit.sp 39 | import coil.ImageLoader 40 | import coil.compose.rememberAsyncImagePainter 41 | 42 | 43 | @Composable 44 | fun MediaItem( 45 | pickerFile: PickerFile = PickerFile(""), 46 | config: PickerConfig = PickerConfig(PickerType.Video), 47 | pickerMode: PickerMode = PickerMode(PickerType.Video), 48 | imageLoader: ImageLoader, 49 | onChecked: () -> Unit = {}, 50 | ) { 51 | val context = LocalContext.current 52 | val painter = rememberAsyncImagePainter(model = pickerFile.path, imageLoader = imageLoader) 53 | 54 | Box( 55 | modifier = Modifier 56 | .padding(8.dp) 57 | .clickable { onChecked() } 58 | .background(Color.Gray, RoundedCornerShape(16.dp)) 59 | .aspectRatio(1f) 60 | ) { 61 | Image( 62 | painter = painter, 63 | contentDescription = null, 64 | contentScale = ContentScale.Crop, 65 | modifier = Modifier 66 | .clickable { if (config.showPreview) 67 | PickerUtils.openFile(context, pickerFile.path) 68 | else 69 | onChecked() } 70 | .fillMaxSize() 71 | .clip(RoundedCornerShape(10.dp)) 72 | ) 73 | 74 | CircleCheckbox( 75 | modifier = Modifier.align(Alignment.TopEnd), 76 | selected = pickerFile.selected, 77 | selectedColor = config.checkBoxSelectedColor, 78 | unSelectedColor = config.checkBoxUnSelectedColor, 79 | iconSize = config.checkBoxSize, 80 | onChecked = onChecked 81 | ) 82 | 83 | if (pickerMode.pickerType == PickerType.Video) 84 | Icon( 85 | painter = painterResource(id = config.videoPlayIcon), 86 | contentDescription = null, 87 | Modifier 88 | .size(config.videoPlayIconSize) 89 | .shadow(2.dp, CircleShape, spotColor = pickerColorPrimary) 90 | .align(Alignment.Center) 91 | .background(config.videoPlayIconBackground, CircleShape) 92 | .padding(8.dp), 93 | tint = config.videoPlayIconTint, 94 | ) 95 | } 96 | } 97 | 98 | @Preview(showBackground = true) 99 | @Composable 100 | fun MediaAudioItem( 101 | pickerFile: PickerFile = PickerFile(""), 102 | config: PickerConfig = PickerConfig(PickerType.Audio), 103 | pickerMode: PickerMode = PickerMode(PickerType.Audio), 104 | onChecked: () -> Unit = {} 105 | 106 | ) { 107 | val context = LocalContext.current 108 | Row( 109 | modifier = Modifier 110 | .fillMaxWidth() 111 | .clickable { onChecked() } 112 | .padding(vertical = 8.dp), 113 | horizontalArrangement = Arrangement.SpaceBetween, 114 | verticalAlignment = Alignment.CenterVertically, 115 | ) { 116 | Icon( 117 | painter = painterResource(id = pickerMode.itemIcon), 118 | contentDescription = null, 119 | modifier = Modifier 120 | .clickable { 121 | if (config.showPreview) 122 | PickerUtils.openFile(context, pickerFile.path) 123 | else 124 | onChecked() 125 | } 126 | .size(pickerMode.itemIconSize) 127 | .background(pickerMode.itemIconBackground, CircleShape) 128 | .padding(4.dp), 129 | tint = pickerMode.itemIconTint 130 | ) 131 | 132 | Spacer(modifier = Modifier.width(8.dp)) 133 | 134 | Text( 135 | modifier = Modifier 136 | .clickable { 137 | if (config.showPreview) 138 | PickerUtils.openFile(context, pickerFile.path) 139 | else 140 | onChecked() 141 | } 142 | .weight(1f, true), 143 | text = pickerFile.file.name, 144 | maxLines = 1, 145 | fontSize = 14.sp, 146 | color = Color.White, 147 | textAlign = TextAlign.Left 148 | ) 149 | 150 | CircleCheckbox( 151 | selected = pickerFile.selected, 152 | selectedColor = config.checkBoxSelectedColor, 153 | unSelectedColor = config.checkBoxUnSelectedColor, 154 | iconSize = config.checkBoxSize, 155 | onChecked = onChecked, 156 | ) 157 | } 158 | 159 | } 160 | 161 | 162 | @Preview(showBackground = true) 163 | @Composable 164 | fun FileItem( 165 | icon: Int = R.drawable.mahdiasd_ic_storage, 166 | iconBackground: Color = Color.White, 167 | iconTint: Color = lightPurple, 168 | iconSize: Dp = 52.dp, 169 | 170 | title: String = "", 171 | titleTextStyle: TextStyle = TextStyle(), 172 | 173 | description: String = "", 174 | descriptionTextStyle: TextStyle = TextStyle(), 175 | 176 | supportRtl: Boolean = false, 177 | 178 | onClicked: () -> Unit = {} 179 | ) { 180 | Row( 181 | modifier = Modifier 182 | .padding(vertical = 8.dp, horizontal = 16.dp) 183 | .fillMaxWidth() 184 | .clickable { onClicked() } 185 | .padding(vertical = 8.dp), 186 | horizontalArrangement = Arrangement.SpaceBetween, 187 | verticalAlignment = Alignment.CenterVertically, 188 | ) { 189 | if (supportRtl) { 190 | Column(modifier = Modifier.weight(1f, true), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { 191 | Text( 192 | modifier = Modifier 193 | .fillMaxWidth(), 194 | text = title, 195 | style = titleTextStyle.plus(TextStyle(textDirection = TextDirection.Rtl, textAlign = TextAlign.Right)) 196 | ) 197 | Text( 198 | modifier = Modifier 199 | .fillMaxWidth(), 200 | text = description, 201 | style = descriptionTextStyle.plus(TextStyle(textDirection = TextDirection.Rtl, textAlign = TextAlign.Right)) 202 | ) 203 | } 204 | Spacer(modifier = Modifier.width(8.dp)) 205 | Icon( 206 | painter = painterResource(id = icon), 207 | contentDescription = null, 208 | modifier = Modifier 209 | .size(iconSize) 210 | .background(iconBackground, CircleShape) 211 | .padding(8.dp), 212 | tint = iconTint 213 | ) 214 | } else { 215 | Icon( 216 | painter = painterResource(id = icon), 217 | contentDescription = null, 218 | modifier = Modifier 219 | .size(iconSize) 220 | .background(iconBackground, CircleShape) 221 | .padding(8.dp), 222 | tint = iconTint 223 | ) 224 | Spacer(modifier = Modifier.width(8.dp)) 225 | Column(modifier = Modifier.weight(1f, true)) { 226 | Text( 227 | text = title, 228 | style = titleTextStyle 229 | ) 230 | Text( 231 | text = description, 232 | style = descriptionTextStyle 233 | ) 234 | } 235 | } 236 | 237 | 238 | } 239 | 240 | } 241 | 242 | @Composable 243 | fun CircleCheckbox( 244 | modifier: Modifier = Modifier, 245 | selected: Boolean, 246 | selectedColor: Color, 247 | unSelectedColor: Color, 248 | iconSize: Dp = 24.dp, 249 | onChecked: () -> Unit = {}, 250 | ) { 251 | Icon( 252 | imageVector = Icons.Filled.Done, 253 | tint = Color.White, 254 | modifier = modifier 255 | .size(iconSize) 256 | .padding(4.dp) 257 | .size(24.dp) 258 | .shadow(2.dp, CircleShape) 259 | .background(if (selected) selectedColor else unSelectedColor, shape = CircleShape) 260 | .padding(4.dp) 261 | .clickable { 262 | onChecked() 263 | }, 264 | contentDescription = "checkbox" 265 | ) 266 | 267 | } 268 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/java/mahdiasd/bottomdialogfilepicker/PickerConfig.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import androidx.compose.foundation.shape.CircleShape 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.Shape 7 | import androidx.compose.ui.text.TextStyle 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.unit.sp 13 | import java.io.Serializable 14 | 15 | val pickerDarkColor = Color(0xFF121212) 16 | val pickerColorPrimary = Color(0xFF55D6D6) 17 | 18 | 19 | @Stable 20 | data class PickerConfig( 21 | val currentType: PickerType, 22 | val modes: List = PickerUtils.allModes, 23 | 24 | val containerColor: Color = pickerDarkColor, 25 | val scrimColor: Color? = null, 26 | val maxSelection: Int = 100, 27 | val showPreview: Boolean = false, 28 | 29 | val enableCamera: Boolean = true, 30 | val cameraIcon: Int = R.drawable.mahdiasd_ic_camera, 31 | val cameraIconTint: Color = Color.White, 32 | val cameraIconBackground: Color = Color.Transparent, 33 | 34 | val checkBoxSelectedColor: Color = pickerColorPrimary, 35 | val checkBoxUnSelectedColor: Color = Color.DarkGray, 36 | val checkBoxSize: Dp = 36.dp, 37 | 38 | val doneIcon: Int = R.drawable.mahdiasd_ic_tick, 39 | val doneIconSize: Dp = 52.dp, 40 | val doneIconTint: Color = Color.White, 41 | val doneIconBackground: Color = pickerColorPrimary, 42 | 43 | val doneBadgeBackgroundColor: Color = pickerColorPrimary, 44 | val doneBadgeStyle: TextStyle = TextStyle(Color.White, fontWeight = FontWeight.Normal, fontSize = 14.sp, textAlign = TextAlign.Center), 45 | 46 | 47 | val videoPlayIcon: Int = R.drawable.mahdiasd_ic_play, 48 | val videoPlayIconSize: Dp = 28.dp, 49 | val videoPlayIconTint: Color = Color.DarkGray, 50 | val videoPlayIconBackground: Color = Color.White, 51 | 52 | val storageIcon: Int = R.drawable.mahdiasd_ic_storage, 53 | val storageIconSize: Dp = 48.dp, 54 | val storageIconTint: Color = Color.White, 55 | val storageIconBackground: Color = amber, 56 | val storageTitle: String = "Storage", 57 | val storageDescription: String = "Brows your file system", 58 | val storageTitleStyle: TextStyle = TextStyle(Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp), 59 | val storageDescriptionStyle: TextStyle = TextStyle(Color.Gray, fontWeight = FontWeight.Normal, fontSize = 14.sp), 60 | 61 | val galleryIcon: Int = R.drawable.mahdiasd_ic_gallery, 62 | val galleryIconSize: Dp = 48.dp, 63 | val galleryIconTint: Color = Color.White, 64 | val galleryIconBackground: Color = lightPurple, 65 | val galleryTitle: String = "Gallery", 66 | val galleryDescription: String = "To send images directly from gallery", 67 | val galleryTitleStyle: TextStyle = TextStyle(Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp), 68 | val galleryDescriptionStyle: TextStyle = TextStyle(Color.Gray, fontWeight = FontWeight.Normal, fontSize = 14.sp), 69 | val supportRtl: Boolean = false, 70 | 71 | val searchTextHint: String = "Search", 72 | val searchTextHintStyle: TextStyle = TextStyle(color = Color.Gray, fontSize = 14.sp), 73 | val searchTextStyle: TextStyle = TextStyle(color = Color.White, fontSize = 14.sp), 74 | 75 | val noItemMessage: String = "No item to show", 76 | val noItemStyle: TextStyle = TextStyle(Color.Cyan, fontWeight = FontWeight.Normal, fontSize = 14.sp), 77 | 78 | ) { 79 | } 80 | 81 | @Stable 82 | data class PickerMode( 83 | val pickerType: PickerType, 84 | 85 | val title: String = getDefaultTitle(pickerType), 86 | 87 | val selectedColor: Color = pickerColorPrimary, 88 | 89 | val titleStyle: TextStyle = TextStyle(Color.White, fontWeight = FontWeight.Normal, fontSize = 14.sp), 90 | val itemTextStyle: TextStyle = TextStyle(Color.White, fontWeight = FontWeight.Normal, fontSize = 14.sp), 91 | 92 | val itemIcon: Int = R.drawable.mahdiasd_ic_music, 93 | val itemIconSize: Dp = 32.dp, 94 | val itemIconTint: Color = pickerColorPrimary, 95 | val itemIconBackground: Color = Color.White, 96 | 97 | 98 | val icon: Int = getDefaultIcon(pickerType), 99 | val iconTint: Color = Color.White, 100 | val iconSize: Dp = 58.dp, 101 | 102 | val shape: Shape = CircleShape, 103 | val shapeColor: Color = getDefaultColor(pickerType) 104 | ) : Serializable 105 | 106 | private fun getDefaultColor(pickerType: PickerType): Color { 107 | return when (pickerType) { 108 | PickerType.Image -> darkBlue 109 | PickerType.Video -> lightGreen 110 | PickerType.File -> deepOrange 111 | PickerType.Audio -> amber 112 | } 113 | } 114 | 115 | private fun getDefaultTitle(pickerType: PickerType): String { 116 | return when (pickerType) { 117 | PickerType.Image -> "Image" 118 | PickerType.Video -> "Video" 119 | PickerType.File -> "File" 120 | PickerType.Audio -> "Music" 121 | } 122 | } 123 | 124 | private fun getDefaultIcon(pickerType: PickerType): Int { 125 | return when (pickerType) { 126 | PickerType.Image -> R.drawable.mahdiasd_ic_image 127 | PickerType.Video -> R.drawable.mahdiasd_ic_video 128 | PickerType.File -> R.drawable.mahdiasd_ic_file 129 | PickerType.Audio -> R.drawable.mahdiasd_ic_play 130 | } 131 | } 132 | 133 | val darkBlue = Color(0xFF3F51B5) 134 | val lightBlue = Color(0xFF03A9F4) 135 | val lightGreen = Color(0xFF4CAF50) 136 | val deepOrange = Color(0xFFFF7043) 137 | val amber = Color(0xFFFFC107) 138 | val lightPurple = Color(0xFFBA68C8) 139 | 140 | enum class PickerType { Image, Video, File, Audio, } -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/java/mahdiasd/bottomdialogfilepicker/PickerFile.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import androidx.compose.runtime.Stable 4 | import java.io.File 5 | import java.io.Serializable 6 | 7 | @Stable 8 | data class PickerFile( 9 | val path: String, 10 | val file: File = File(path), 11 | val selected: Boolean = false 12 | ) : Serializable 13 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/java/mahdiasd/bottomdialogfilepicker/PickerUtils.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.database.Cursor 6 | import android.graphics.Bitmap 7 | import android.os.Build 8 | import android.os.Environment 9 | import android.provider.MediaStore 10 | import android.util.Log 11 | import android.webkit.MimeTypeMap 12 | import android.widget.Toast 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.platform.LocalDensity 15 | import androidx.compose.ui.unit.Dp 16 | import androidx.compose.ui.unit.dp 17 | import androidx.core.content.FileProvider 18 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 19 | import com.google.accompanist.permissions.MultiplePermissionsState 20 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 21 | import java.io.File 22 | import java.io.FileOutputStream 23 | 24 | object PickerUtils { 25 | 26 | val allModes = listOf( 27 | PickerMode(PickerType.Image, title = "عکس"), 28 | PickerMode(PickerType.Video, "ویدیو"), 29 | PickerMode(PickerType.File, "فایل"), 30 | PickerMode(PickerType.Audio, "موزیک"), 31 | ) 32 | 33 | @OptIn(ExperimentalPermissionsApi::class) 34 | @Composable 35 | fun permissionState(enableCamera: Boolean): MultiplePermissionsState { 36 | val list = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 37 | mutableListOf( 38 | android.Manifest.permission.READ_MEDIA_IMAGES, 39 | android.Manifest.permission.READ_MEDIA_VIDEO, 40 | android.Manifest.permission.READ_MEDIA_AUDIO 41 | ) 42 | } else { 43 | mutableListOf( 44 | android.Manifest.permission.READ_EXTERNAL_STORAGE, 45 | ) 46 | } 47 | 48 | if (enableCamera) list.add(android.Manifest.permission.CAMERA) 49 | 50 | return rememberMultiplePermissionsState(list) 51 | } 52 | 53 | fun getImage(context: Context, enableCamera: Boolean): List { 54 | val list = mutableListOf() 55 | if (enableCamera) { 56 | list.add(PickerFile("show camera")) 57 | } 58 | val columns = arrayOf(MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID) 59 | 60 | val orderBy = MediaStore.Images.Media.DATE_ADDED + " DESC" 61 | 62 | val cursor: Cursor = context.contentResolver.query( 63 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 64 | columns, 65 | null, 66 | null, 67 | orderBy 68 | ) ?: return emptyList() 69 | 70 | while (cursor.moveToNext()) { 71 | val dataColumnIndex: Int = cursor.getColumnIndex(MediaStore.Images.Media.DATA) 72 | list.add(PickerFile(cursor.getString(dataColumnIndex))) 73 | } 74 | 75 | cursor.close() 76 | return list 77 | } 78 | 79 | fun getVideo(context: Context): List { 80 | val list = mutableListOf() 81 | val columns = arrayOf( 82 | MediaStore.Video.VideoColumns.DATA, 83 | MediaStore.Video.VideoColumns._ID 84 | ) 85 | 86 | val orderBy = MediaStore.Video.VideoColumns.DATE_ADDED + " DESC" 87 | 88 | val cursor: Cursor = context.contentResolver.query( 89 | /* uri = */ MediaStore.Video.Media.EXTERNAL_CONTENT_URI, /* projection = */ columns, /* selection = */ null, 90 | /* selectionArgs = */ null, /* sortOrder = */ orderBy 91 | ) ?: return emptyList() 92 | 93 | while (cursor.moveToNext()) { 94 | val dataColumnIndex: Int = 95 | cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA) 96 | list.add(PickerFile(cursor.getString(dataColumnIndex))) 97 | } 98 | cursor.close() 99 | return list 100 | } 101 | 102 | fun getAudio(context: Context): List { 103 | val list = mutableListOf() 104 | val columns = arrayOf( 105 | MediaStore.Audio.AudioColumns._ID, 106 | MediaStore.Audio.AudioColumns.DATA, 107 | ) 108 | val orderBy = MediaStore.Audio.AudioColumns.DATE_ADDED + " DESC" 109 | 110 | val cursor = context.contentResolver.query( 111 | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 112 | columns, 113 | null, 114 | null, 115 | orderBy 116 | ) ?: return emptyList() 117 | 118 | while (cursor.moveToNext()) { 119 | val dataColumnIndex: Int = cursor.getColumnIndex(MediaStore.Audio.Media.DATA) 120 | list.add(PickerFile(cursor.getString(dataColumnIndex))) 121 | } 122 | cursor.close() 123 | return list 124 | } 125 | 126 | 127 | fun saveBitmapToStorage(bitmap: Bitmap?): File? { 128 | if (bitmap == null) return null 129 | 130 | // Generate a unique file name with date 131 | val fileName = "image_${System.currentTimeMillis()}.png" 132 | 133 | val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path 134 | val file = File(imagesDir, fileName) 135 | 136 | return try { 137 | val fos = FileOutputStream(file) 138 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos) 139 | fos.close() 140 | 141 | if (file.exists()) file 142 | else null 143 | } catch (e: Exception) { 144 | null 145 | } 146 | } 147 | 148 | fun openFile(context: Context, fileAddress: String?) { 149 | if (fileAddress == null) return 150 | try { 151 | val file = File(fileAddress) 152 | val map = MimeTypeMap.getSingleton() 153 | val ext = MimeTypeMap.getFileExtensionFromUrl(file.name) 154 | val type = map.getMimeTypeFromExtension(ext) 155 | val intent = Intent(Intent.ACTION_VIEW) 156 | intent.setDataAndType(FileProvider.getUriForFile(context, "${context.packageName}.provider", file), type) 157 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 158 | intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 159 | context.startActivity(intent) 160 | } catch (e: java.lang.Exception) { 161 | Toast.makeText( 162 | context, 163 | "Can`t open file!", 164 | Toast.LENGTH_SHORT 165 | ).show() 166 | } 167 | } 168 | 169 | 170 | @Composable 171 | fun Dp.toPx(): Float { 172 | val density = LocalDensity.current.density 173 | return density * value 174 | } 175 | 176 | @Composable 177 | fun Int.toDp(): Dp { 178 | val density = LocalDensity.current.density 179 | return (this / density).dp 180 | } 181 | 182 | fun Any?.printToLog(plusTag: String = "", tag: String = "MyLog") { 183 | Log.d(tag, plusTag + " " + toString()) 184 | } 185 | 186 | } -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_audio.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_camera.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_camera_shutter.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_change_camera.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_checkmark.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_close.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_document.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_done.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_file.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_gallery.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_image.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_music.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_play.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_search.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_send.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_storage.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_tick.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/drawable/mahdiasd_ic_video.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /BottomDialogFilePicker/src/test/java/mahdiasd/bottomdialogfilepicker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Xml Version](https://github.com/mahdiasd/BottomDialogFilePicker) 2 | 3 | # Bottom Dialog Android Picker 4 | 5 | [![](https://jitpack.io/v/mahdiasd/BottomDialogFilePicker.svg)](https://jitpack.io/#mahdiasd/BottomDialogFilePicker) 6 | 7 | Bottom dialog picker like telegram for all version of android (1 ... , 10 , 11 , 12 , 13) 8 | 9 | Take picture with camera and save to storage 10 | 11 | Search in Files 12 | 13 | Support android 10+ 14 | 15 | Expandable and scrollable dialog 16 | 17 | Full Customisable (Color , text , minimum and maximum selected file size , ...) 18 | 19 | No required runtime permission 20 | 21 | 22 | ## Screenshots 23 | 24 | ![demo](https://raw.githubusercontent.com/mahdiasd/BottomDialogFilePicker/master/screenshot/1.png) 25 | ![demo](https://raw.githubusercontent.com/mahdiasd/BottomDialogFilePicker/master/screenshot/2.png) 26 | ![demo](https://raw.githubusercontent.com/mahdiasd/BottomDialogFilePicker/master/screenshot/3.png) 27 | ![demo](https://raw.githubusercontent.com/mahdiasd/BottomDialogFilePicker/master/screenshot/4.png) 28 | ![demo](https://raw.githubusercontent.com/mahdiasd/BottomDialogFilePicker/master/screenshot/5.png) 29 | 30 | ## Installation 31 | 32 | #### Step 1. Add the JitPack repository to your build file 33 | 34 | Install my project with gradle 35 | Add it in your root build.gradle at the end of repositories: 36 | 37 | 38 | ```bash 39 | allprojects { 40 | repositories { 41 | ... 42 | maven { url 'https://jitpack.io' } 43 | } 44 | } 45 | ``` 46 | #### Step 2. Add the dependency 47 | 48 | ```bash 49 | dependencies { 50 | implementation 'com.github.mahdiasd:ComposeBottomDialogFilePicker:1.0.1' 51 | } 52 | ``` 53 | ## Ho To Use 54 | 55 | ``` 56 | val isShowButtomDialog = remember { mutableStateOf(true) } 57 | 58 | val config = PickerConfig( 59 | currentType = PickerType.Image, 60 | modes = listOf(PickerMode(PickerType.Image, title = "عکس")), // you can choose Image, Video, File, Audio and customize each picker mode 61 | storageTitle = "حافظه دستگاه", 62 | storageDescription = "برای انتخاب فایل از فایل منیجر دستگاه", 63 | galleryTitle = "گالری", 64 | galleryDescription = "برای انتخاب فایل از گالری دستگاه", 65 | supportRtl = true, 66 | maxSelection = 12, 67 | searchTextHint = "جستجو", 68 | searchTextHintStyle = TextStyle(textAlign = TextAlign.Right) 69 | ) 70 | 71 | FilePickerDialog( 72 | config = config, 73 | onDismissDialog = { isShowButtomDialog.value = false }, 74 | selectedFiles = { 75 | it.printToLog("selectedFiles") 76 | } 77 | ) 78 | ``` 79 | 80 | ## Customize: 81 | Refer to this link to learn about the parameters that can be customized: 82 | https://github.com/mahdiasd/ComposeBottomDialogFilePicker/blob/master/BottomDialogFilePicker/src/main/java/mahdiasd/bottomdialogfilepicker/PickerConfig.kt 83 | 84 | ## LICENCE 85 | ``` 86 | Copyright 2022 Mahdi Asadollahpour BottomDialogFilePicker 87 | 88 | Licensed under the Apache License, Version 2.0 (the "License"); 89 | you may not use this file except in compliance with the License. 90 | You may obtain a copy of the License at 91 | 92 | http://www.apache.org/licenses/LICENSE-2.0 93 | 94 | Unless required by applicable law or agreed to in writing, software 95 | distributed under the License is distributed on an "AS IS" BASIS, 96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | See the License for the specific language governing permissions and 98 | limitations under the License. 99 | 100 | ``` 101 | 102 | ### | ~~~~ Thank you for your support of my project and star it ~~~~ | 103 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.androidApplication) 4 | alias(libs.plugins.kotlinAndroid) 5 | } 6 | 7 | android { 8 | 9 | 10 | namespace = "mahdiasd.bottomdialogfilepicker.sample" 11 | compileSdk = 33 12 | 13 | defaultConfig { 14 | applicationId = "mahdiasd.bottomdialogfilepicker.sample" 15 | minSdk = 21 16 | targetSdk = 33 17 | versionCode = 1 18 | versionName = "1.0" 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | vectorDrawables { 22 | useSupportLibrary = true 23 | } 24 | } 25 | 26 | buildTypes { 27 | release { 28 | isMinifyEnabled = true 29 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 30 | // signingConfig = signingConfigs.getByName("release") 31 | } 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_1_8 36 | targetCompatibility = JavaVersion.VERSION_1_8 37 | } 38 | kotlinOptions { 39 | jvmTarget = "1.8" 40 | } 41 | buildFeatures { 42 | compose = true 43 | } 44 | composeOptions { 45 | kotlinCompilerExtensionVersion = "1.4.3" 46 | } 47 | packaging { 48 | resources { 49 | // excludes += "/META-INF/{AL2.0,LGPL2.1}" 50 | // excludes += "META-INF/gradle/incremental.annotation.processors" 51 | } 52 | } 53 | } 54 | 55 | dependencies { 56 | 57 | implementation(libs.core.ktx) 58 | implementation(libs.lifecycle.runtime.ktx) 59 | implementation(libs.activity.compose) 60 | implementation(platform(libs.compose.bom)) 61 | 62 | implementation(libs.ui) 63 | implementation(libs.ui.graphics) 64 | implementation(libs.ui.tooling.preview) 65 | implementation(libs.material3) 66 | 67 | implementation(project(mapOf("path" to ":BottomDialogFilePicker"))) 68 | 69 | testImplementation(libs.junit) 70 | androidTestImplementation(libs.androidx.test.ext.junit) 71 | androidTestImplementation(libs.espresso.core) 72 | androidTestImplementation(platform(libs.compose.bom)) 73 | androidTestImplementation(libs.ui.test.junit4) 74 | debugImplementation(libs.ui.tooling) 75 | debugImplementation(libs.ui.test.manifest) 76 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/mahdiasd/bottomdialogfilepicker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("mahdiasd.bottomdialogfilepicker", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 18 | 19 | 20 | 21 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/java/mahdiasd/bottomdialogfilepicker/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.os.Bundle 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.DisposableEffect 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.style.TextAlign 22 | import mahdiasd.bottomdialogfilepicker.PickerUtils.printToLog 23 | import mahdiasd.bottomdialogfilepicker.ui.theme.BottomDialogFilePickerTheme 24 | 25 | class MainActivity : ComponentActivity() { 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | 30 | val config = PickerConfig( 31 | currentType = PickerType.Image, 32 | storageTitle = "حافظه دستگاه", 33 | storageDescription = "برای انتخاب فایل از فایل منیجر دستگاه", 34 | galleryTitle = "گالری", 35 | galleryDescription = "برای انتخاب فایل از گالری دستگاه", 36 | supportRtl = true, 37 | maxSelection = 12, 38 | searchTextHint = "جستجو", 39 | searchTextHintStyle = TextStyle(textAlign = TextAlign.Right) 40 | ) 41 | 42 | setContent { 43 | BottomDialogFilePickerTheme { 44 | // LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) 45 | val isShowing = remember { 46 | mutableStateOf(true) 47 | } 48 | Surface( 49 | modifier = Modifier.fillMaxSize(), 50 | color = MaterialTheme.colorScheme.background 51 | ) { 52 | if (isShowing.value) { 53 | FilePickerDialog( 54 | config = config, 55 | onDismissDialog = { 56 | isShowing.value = false 57 | }, 58 | selectedFiles = { 59 | it.printToLog("selectedFiles") 60 | } 61 | ) 62 | } else { 63 | Button(onClick = { isShowing.value = true }) { 64 | Text(text = "Open Dialog") 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | } 73 | @Composable 74 | fun LockScreenOrientation(orientation: Int) { 75 | val context = LocalContext.current 76 | DisposableEffect(orientation) { 77 | val activity = context.findActivity() ?: return@DisposableEffect onDispose {} 78 | val originalOrientation = activity.requestedOrientation 79 | activity.requestedOrientation = orientation 80 | onDispose { 81 | // restore original orientation when view disappears 82 | activity.requestedOrientation = originalOrientation 83 | } 84 | } 85 | } 86 | fun Context.findActivity(): Activity? = when (this) { 87 | is Activity -> this 88 | is ContextWrapper -> baseContext.findActivity() 89 | else -> null 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/mahdiasd/bottomdialogfilepicker/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | 13 | 14 | val pickerDarkColor = Color(0xFF121212) 15 | val pickerColorPrimary = Color(0xFF55D6D6) 16 | -------------------------------------------------------------------------------- /app/src/main/java/mahdiasd/bottomdialogfilepicker/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker.ui.theme 2 | 3 | import android.app.Activity 4 | import android.graphics.Color.toArgb 5 | import android.os.Build 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.darkColorScheme 9 | import androidx.compose.material3.dynamicDarkColorScheme 10 | import androidx.compose.material3.dynamicLightColorScheme 11 | import androidx.compose.material3.lightColorScheme 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.SideEffect 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.toArgb 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.platform.LocalView 18 | import androidx.core.view.WindowCompat 19 | 20 | private val DarkColorScheme = darkColorScheme( 21 | primary = Purple80, 22 | secondary = PurpleGrey80, 23 | tertiary = Pink80 24 | ) 25 | 26 | private val LightColorScheme = lightColorScheme( 27 | primary = Purple40, 28 | secondary = PurpleGrey40, 29 | tertiary = Pink40 30 | 31 | /* Other default colors to override 32 | background = Color(0xFFFFFBFE), 33 | surface = Color(0xFFFFFBFE), 34 | onPrimary = Color.White, 35 | onSecondary = Color.White, 36 | onTertiary = Color.White, 37 | onBackground = Color(0xFF1C1B1F), 38 | onSurface = Color(0xFF1C1B1F), 39 | */ 40 | ) 41 | 42 | @Composable 43 | fun BottomDialogFilePickerTheme( 44 | darkTheme: Boolean = isSystemInDarkTheme(), 45 | // Dynamic color is available on Android 12+ 46 | dynamicColor: Boolean = true, 47 | content: @Composable () -> Unit 48 | ) { 49 | val colorScheme = when { 50 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 51 | val context = LocalContext.current 52 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 53 | } 54 | 55 | darkTheme -> DarkColorScheme 56 | else -> LightColorScheme 57 | } 58 | val view = LocalView.current 59 | if (!view.isInEditMode) { 60 | SideEffect { 61 | val window = (view.context as Activity).window 62 | window.statusBarColor = colorScheme.primary.toArgb() 63 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 64 | } 65 | } 66 | 67 | MaterialTheme( 68 | colorScheme = colorScheme, 69 | typography = Typography, 70 | content = content 71 | ) 72 | } 73 | 74 | 75 | @Composable 76 | fun EnableFullScreen() { 77 | val view = LocalView.current 78 | SideEffect { 79 | val window = (view.context as Activity).window 80 | window.statusBarColor = Color.Transparent.toArgb() 81 | WindowCompat.setDecorFitsSystemWindows(window, false) 82 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false 83 | } 84 | } 85 | 86 | @Composable 87 | fun DisableFullScreen( 88 | statusBarColor: Color = Color.Black, 89 | navigationBarColor: Color = Color.Black 90 | ) { 91 | val view = LocalView.current 92 | SideEffect { 93 | val window = (view.context as Activity).window 94 | window.statusBarColor = statusBarColor.toArgb() 95 | window.navigationBarColor = navigationBarColor.toArgb() 96 | WindowCompat.setDecorFitsSystemWindows(window, true) 97 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/mahdiasd/bottomdialogfilepicker/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BottomDialogFilePicker Sample 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/mahdiasd/bottomdialogfilepicker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package mahdiasd.bottomdialogfilepicker 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 3 | plugins { 4 | alias(libs.plugins.androidApplication) apply false 5 | alias(libs.plugins.kotlinAndroid) apply false 6 | alias(libs.plugins.android.library) apply false 7 | } 8 | true // Needed to make the Suppress annotation work for the plugins block -------------------------------------------------------------------------------- /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=-Xmx2048m -Dfile.encoding=UTF-8 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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.0.2" 3 | coil-video = "2.4.0" 4 | handle-path-oz = "1.0.7" 5 | kotlin = "1.8.10" 6 | core-ktx = "1.10.1" 7 | junit = "4.13.2" 8 | androidx-test-ext-junit = "1.1.5" 9 | espresso-core = "3.5.1" 10 | lifecycle-runtime-ktx = "2.6.1" 11 | activity-compose = "1.7.2" 12 | compose-bom = "2023.06.01" 13 | material = "1.9.0" 14 | constraintlayout = "1.0.1" 15 | kotlinx-collections-immutable = "0.3.5" 16 | coil-compose = "2.4.0" 17 | accompanist = "0.30.1" 18 | appcompat = "1.6.1" 19 | 20 | [libraries] 21 | coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil-video" } 22 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } 23 | handle-path-oz = { module = "com.github.onimur:handle-path-oz", version.ref = "handle-path-oz" } 24 | junit = { group = "junit", name = "junit", version.ref = "junit" } 25 | androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } 26 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } 27 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } 28 | activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } 29 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } 30 | material = { module = "com.google.android.material:material", version.ref = "material" } 31 | ui = { group = "androidx.compose.ui", name = "ui" } 32 | ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 33 | ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 34 | ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 35 | ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 36 | ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } 37 | material3 = { group = "androidx.compose.material3", name = "material3" } 38 | constraintlayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayout" } 39 | kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" } 40 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" } 41 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } 42 | 43 | androidx-camera-core = { group = "androidx.camera", name = "camera-core", version = "1.2.3" } 44 | androidx-camera-view = { group = "androidx.camera", name = "camera-view", version = "1.2.3" } 45 | androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version = "1.2.3" } 46 | androidx-camera-camera2 = { group = "androidx.camera", name = "camera2", version = "1.2.3" } 47 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 48 | 49 | [plugins] 50 | androidApplication = { id = "com.android.application", version.ref = "agp" } 51 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 52 | android-library = { id = "com.android.library", version.ref = "agp" } 53 | 54 | [bundles] 55 | 56 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 23 10:05:20 GMT+03:30 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | -openjdk17 -------------------------------------------------------------------------------- /key.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahdiasd/ComposeBottomDialogFilePicker/8ad19605e375e30eed42bc4d4c00417f7b5c507c/key.jks -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.net.URI 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | maven { url = URI("https://jitpack.io") } 16 | 17 | } 18 | } 19 | 20 | rootProject.name = "BottomDialogFilePicker" 21 | include(":app") 22 | include(":BottomDialogFilePicker") 23 | --------------------------------------------------------------------------------