Skip to content

Commit

Permalink
🔀 Merge pull request #14 from vinceglb/improve-pick-api
Browse files Browse the repository at this point in the history
♻️ Improve Picker API
  • Loading branch information
vinceglb authored May 2, 2024
2 parents 8ffd000 + 92133c4 commit 49daeee
Show file tree
Hide file tree
Showing 27 changed files with 687 additions and 555 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import io.github.vinceglb.picker.core.Picker
import io.github.vinceglb.picker.core.PickerSelectionMode
import io.github.vinceglb.picker.core.PickerSelectionType
import io.github.vinceglb.picker.core.PlatformDirectory
import io.github.vinceglb.picker.core.PlatformFile
import kotlinx.coroutines.launch

@Composable
public fun <Out> rememberPickerLauncher(
public fun <Out> rememberFilePickerLauncher(
type: PickerSelectionType = PickerSelectionType.File(),
mode: PickerSelectionMode<Out>,
title: String? = null,
initialDirectory: String? = null,
Expand All @@ -24,6 +27,7 @@ public fun <Out> rememberPickerLauncher(
val coroutineScope = rememberCoroutineScope()

// Updated state
val currentType by rememberUpdatedState(type)
val currentMode by rememberUpdatedState(mode)
val currentTitle by rememberUpdatedState(title)
val currentInitialDirectory by rememberUpdatedState(initialDirectory)
Expand All @@ -36,7 +40,8 @@ public fun <Out> rememberPickerLauncher(
val returnedLauncher = remember {
PickerResultLauncher {
coroutineScope.launch {
val result = picker.pick(
val result = picker.pickFile(
type = currentType,
mode = currentMode,
title = currentTitle,
initialDirectory = currentInitialDirectory,
Expand All @@ -50,7 +55,59 @@ public fun <Out> rememberPickerLauncher(
}

@Composable
public fun rememberSaverLauncher(
public fun rememberFilePickerLauncher(
type: PickerSelectionType = PickerSelectionType.File(),
title: String? = null,
initialDirectory: String? = null,
onResult: (PlatformFile?) -> Unit,
): PickerResultLauncher {
return rememberFilePickerLauncher(
type = type,
mode = PickerSelectionMode.Single,
title = title,
initialDirectory = initialDirectory,
onResult = onResult,
)
}

@Composable
public fun rememberDirectoryPickerLauncher(
title: String? = null,
initialDirectory: String? = null,
onResult: (PlatformDirectory?) -> Unit,
): PickerResultLauncher {
// Init picker
InitPicker()

// Coroutine
val coroutineScope = rememberCoroutineScope()

// Updated state
val currentTitle by rememberUpdatedState(title)
val currentInitialDirectory by rememberUpdatedState(initialDirectory)
val currentOnResult by rememberUpdatedState(onResult)

// Picker
val picker = remember { Picker }

// Picker launcher
val returnedLauncher = remember {
PickerResultLauncher {
coroutineScope.launch {
val result = picker.pickDirectory(
title = currentTitle,
initialDirectory = currentInitialDirectory,
)
currentOnResult(result)
}
}
}

return returnedLauncher
}

@Composable
public fun rememberFileSaverLauncher(
onResult: (PlatformFile?) -> Unit
): SaverResultLauncher {
// Init picker
Expand All @@ -69,7 +126,7 @@ public fun rememberSaverLauncher(
val returnedLauncher = remember {
SaverResultLauncher { bytes, baseName, extension, initialDirectory ->
coroutineScope.launch {
val result = picker.save(
val result = picker.saveFile(
bytes = bytes,
baseName = baseName,
extension = extension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import io.github.vinceglb.picker.core.PickerSelectionMode.SelectionResult
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
Expand All @@ -16,66 +20,122 @@ import kotlin.coroutines.suspendCoroutine

public actual object Picker {
private var registry: ActivityResultRegistry? = null
internal var context: WeakReference<Context?> = WeakReference(null)
private set
private var context: WeakReference<Context?> = WeakReference(null)

public fun init(activity: ComponentActivity) {
context = WeakReference(activity.applicationContext)
registry = activity.activityResultRegistry
}

public actual suspend fun <Out> pick(
public actual suspend fun <Out> pickFile(
type: PickerSelectionType,
mode: PickerSelectionMode<Out>,
title: String?,
initialDirectory: String?,
initialDirectory: String?
): Out? = withContext(Dispatchers.IO) {
// Throw exception if registry is not initialized
val registry = registry ?: throw PickerNotInitializedException()

// It doesn't really matter what the key is, just that it is unique
val key = UUID.randomUUID().toString()

// Open native file picker
val selection = suspendCoroutine { continuation ->
when (mode) {
is PickerSelectionMode.SingleFile -> {
val contract = ActivityResultContracts.OpenDocument()
val launcher = registry.register(key, contract) { uri ->
continuation.resume(SelectionResult(
files = uri?.let { listOf(it) }
))
// Get context
val context = Picker.context.get()
?: throw PickerNotInitializedException()

val result: PlatformFiles? = suspendCoroutine { continuation ->
when (type) {
PickerSelectionType.Image,
PickerSelectionType.Video,
PickerSelectionType.ImageAndVideo -> {
when (mode) {
is PickerSelectionMode.Single -> {
val contract = PickVisualMedia()
val launcher = registry.register(key, contract) { uri ->
val result = uri?.let { listOf(PlatformFile(it, context)) }
continuation.resume(result)
}

val request = when (type) {
PickerSelectionType.Image -> PickVisualMediaRequest(ImageOnly)
PickerSelectionType.Video -> PickVisualMediaRequest(VideoOnly)
PickerSelectionType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo)
else -> throw IllegalArgumentException("Unsupported type: $type")
}

launcher.launch(request)
}

is PickerSelectionMode.Multiple -> {
val contract = ActivityResultContracts.PickMultipleVisualMedia()
val launcher = registry.register(key, contract) { uri ->
val result = uri.map { PlatformFile(it, context) }
continuation.resume(result)
}

val request = when (type) {
PickerSelectionType.Image -> PickVisualMediaRequest(ImageOnly)
PickerSelectionType.Video -> PickVisualMediaRequest(VideoOnly)
PickerSelectionType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo)
else -> throw IllegalArgumentException("Unsupported type: $type")
}

launcher.launch(request)
}
}
launcher.launch(getMimeTypes(mode.extensions))
}

is PickerSelectionMode.MultipleFiles -> {
val contract = ActivityResultContracts.OpenMultipleDocuments()
val launcher = registry.register(key, contract) { uris ->
continuation.resume(SelectionResult(files = uris))
is PickerSelectionType.File -> {
when (mode) {
is PickerSelectionMode.Single -> {
val contract = ActivityResultContracts.OpenDocument()
val launcher = registry.register(key, contract) { uri ->
val result = uri?.let { listOf(PlatformFile(it, context)) }
continuation.resume(result)
}
launcher.launch(getMimeTypes(type.extensions))
}

is PickerSelectionMode.Multiple -> {
val contract = ActivityResultContracts.OpenMultipleDocuments()
val launcher = registry.register(key, contract) { uris ->
val result = uris.map { PlatformFile(it, context) }
continuation.resume(result)
}
launcher.launch(getMimeTypes(type.extensions))
}
}
launcher.launch(getMimeTypes(mode.extensions))
}
}
}

is PickerSelectionMode.Directory -> {
val contract = ActivityResultContracts.OpenDocumentTree()
val launcher = registry.register(key, contract) { uri ->
continuation.resume(SelectionResult(
files = uri?.let { listOf(it) }
))
}
val initialUri = initialDirectory?.let { Uri.parse(it) }
launcher.launch(initialUri)
}
mode.parseResult(result)
}

public actual suspend fun pickDirectory(
title: String?,
initialDirectory: String?
): PlatformDirectory? = withContext(Dispatchers.IO) {
// Throw exception if registry is not initialized
val registry = registry ?: throw PickerNotInitializedException()

// It doesn't really matter what the key is, just that it is unique
val key = UUID.randomUUID().toString()

else -> throw IllegalArgumentException("Unsupported mode: $mode")
suspendCoroutine { continuation ->
val contract = ActivityResultContracts.OpenDocumentTree()
val launcher = registry.register(key, contract) { uri ->
val platformDirectory = uri?.let { PlatformDirectory(it) }
continuation.resume(platformDirectory)
}
val initialUri = initialDirectory?.let { Uri.parse(it) }
launcher.launch(initialUri)
}

// Return result
return@withContext mode.result(selection)
}

public actual suspend fun save(
public actual fun isDirectoryPickerSupported(): Boolean = true

public actual suspend fun saveFile(
bytes: ByteArray,
baseName: String,
extension: String,
Expand Down Expand Up @@ -114,7 +174,7 @@ public actual object Picker {
}
}

public fun getMimeTypes(fileExtensions: List<String>?): Array<String> {
private fun getMimeTypes(fileExtensions: List<String>?): Array<String> {
val mimeTypeMap = MimeTypeMap.getSingleton()
return fileExtensions
?.takeIf { it.isNotEmpty() }
Expand All @@ -123,7 +183,7 @@ public actual object Picker {
?: arrayOf("*/*")
}

public fun getMimeType(fileExtension: String): String {
private fun getMimeType(fileExtension: String): String {
val mimeTypeMap = MimeTypeMap.getSingleton()
return mimeTypeMap.getMimeTypeFromExtension(fileExtension) ?: "*/*"
}
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit 49daeee

Please sign in to comment.