Skip to content

Add ability to monitor file picker import progress #300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 22, 2025

Conversation

kihaki
Copy link
Contributor

@kihaki kihaki commented May 30, 2025

No description provided.

@kihaki kihaki force-pushed the feature/296-picker-progress branch from 9e45fde to aa86b77 Compare May 30, 2025 11:56
@vinceglb
Copy link
Owner

vinceglb commented Jun 6, 2025

Hi @kihaki! That's a really good idea! I'm going to try that 🔥

@vinceglb vinceglb self-requested a review June 6, 2025 11:41
@vinceglb vinceglb marked this pull request as ready for review June 25, 2025 23:33
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: State Update Errors and Duplicate File Handling

The _uiState.update calls incorrectly use the copy() method without named parameters (e.g., files = ...), which may cause a compilation error or update the wrong field. Additionally, FileKitPickerState.Progress and FileKitPickerState.Completed likely provide cumulative lists (result.processed, result.result), leading to duplicate files being added to the _uiState.files list.

samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt#L101-L109

is FileKitPickerState.Started -> println("Started picking ${result.total} files")
is FileKitPickerState.Progress -> {
println("New files processed: ${result.processed.size} / ${result.total}")
_uiState.update { it.copy(it.files + result.processed) }
}
is FileKitPickerState.Completed -> {
println("File picker completed with ${result.result.size} files")
_uiState.update { it.copy(it.files + result.result) }

Fix in Cursor


Bug: File Picker Fails with Empty MIME Types

The getMimeTypes function no longer falls back to ["*/*"] when all provided file extensions fail to map to valid MIME types. This results in an empty MIME type array being passed to the Android file picker, which can prevent it from showing any file types or allowing selection.

filekit-dialogs/src/androidMain/kotlin/io/github/vinceglb/filekit/dialogs/FileKit.android.kt#L226-L234

private fun getMimeTypes(fileExtensions: Set<String>?): Array<String> {
val mimeTypeMap = MimeTypeMap.getSingleton()
return fileExtensions
?.takeIf { it.isNotEmpty() }
?.mapNotNull { mimeTypeMap.getMimeTypeFromExtension(it) }
?.toTypedArray()
?: arrayOf("*/*")
}

Fix in Cursor


Bug: File List Duplication in Picker State Flow

The toPickerStateFlow function's FileKitPickerState.Progress events emit a cumulative list of files processed so far (using subList(0, index + 1)), and FileKitPickerState.Completed events emit the full list. The sample code's consumer logic appends result.processed from Progress and result.result from Completed to its state, causing duplicate files. This requires either Progress to emit only newly processed files, or the consumer to replace the file list or handle only truly new additions.

filekit-dialogs/src/commonMain/kotlin/io/github/vinceglb/filekit/dialogs/Utils.kt#L9-L24

internal fun List<PlatformFile>?.toPickerStateFlow(): Flow<FileKitPickerState<List<PlatformFile>>> {
val files = this
return channelFlow {
when {
files.isNullOrEmpty() -> send(FileKitPickerState.Cancelled)
else -> {
send(FileKitPickerState.Started(files.size))
files.forEachIndexed { index, file ->
send(FileKitPickerState.Progress(files.subList(0, index + 1), files.size))
}
send(FileKitPickerState.Completed(files))
}
}
}
}

samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt#L101-L110

is FileKitPickerState.Started -> println("Started picking ${result.total} files")
is FileKitPickerState.Progress -> {
println("New files processed: ${result.processed.size} / ${result.total}")
_uiState.update { it.copy(it.files + result.processed) }
}
is FileKitPickerState.Completed -> {
println("File picker completed with ${result.result.size} files")
_uiState.update { it.copy(it.files + result.result) }
}

Fix in Cursor


Bug: State Emission Issue in Parsing Function

In SingleWithState.parseResult(), the mapNotNull operator, combined with firstOrNull(), incorrectly filters out Progress and Completed states when their underlying lists (processed or result) are empty. This prevents these states from being emitted, leading to incomplete state transitions and potential flow non-completion.

filekit-dialogs/src/commonMain/kotlin/io/github/vinceglb/filekit/dialogs/FileKitMode.kt#L68-L84

is FileKitPickerState.Progress -> {
it.processed.firstOrNull()?.let { file ->
FileKitPickerState.Progress(
processed = file,
total = it.total
)
}
}
is FileKitPickerState.Completed -> {
it.result.firstOrNull()?.let { file ->
FileKitPickerState.Completed(result = file)
}
}
}
}

Fix in Cursor


BugBot free trial expires on July 22, 2025
You have used $0.00 of your $2.00 spend limit so far. Manage your spend limit in the Cursor dashboard.

Was this report helpful? Give feedback by reacting with 👍 or 👎

@vinceglb
Copy link
Owner

vinceglb commented Jun 25, 2025

Hi @kihaki! I tried to improve the developer experience as much as I could by:

  • keeping FileKit.openFilePicker() and rememberFilePickerLauncher() like before
  • adding new FileKitModes: SingleWithState and MultipleWithState

Also, I finished the implementation on all the platforms, and I fixed a bug on the iOS part.

Here is an example of how to use it:

val multipleFilesPickerWithState = rememberFilePickerLauncher(
    type = FileKitType.Image,
    mode = FileKitMode.MultipleWithState(),
) { result ->
    when (result) {
        FileKitPickerState.Cancelled -> println("File picker cancelled")
        is FileKitPickerState.Started -> println("Started picking ${result.total} files")
        is FileKitPickerState.Progress -> println("New files processed: ${result.processed.size} / ${result.total}")
        is FileKitPickerState.Completed -> println("File picker completed with ${result.result.size} files")
    }
}

Button(onClick = { multipleFilesPickerWithState.launch() }) {
    Text("Picke images with state")
}

and

val files = FileKit.openFilePicker(
    type = FileKitType.Image,
    mode = FileKitMode.MultipleWithState(),
)

files.collect { result ->
    when (result) {
        FileKitPickerState.Cancelled -> println("File picker cancelled")
        is FileKitPickerState.Started -> println("Started picking ${result.total} files")
        is FileKitPickerState.Progress -> println("New files processed: ${result.processed.size} / ${result.total}")
        is FileKitPickerState.Completed -> println("File picker completed with ${result.result.size} files")
    }
}

I'm really interested in having your feedback on my changes!

@vinceglb vinceglb merged commit bd1a064 into vinceglb:main Jul 22, 2025
@kihaki
Copy link
Contributor Author

kihaki commented Aug 2, 2025

Hey @vinceglb I just came back to see how its going, I wasn't notified about your comment it seems, so sorry for not checking in earlier!
Will have a look at the most recent version and give some feedback, very exciting, thanks for adding this!

@vinceglb
Copy link
Owner

vinceglb commented Aug 2, 2025

Hey @kihaki! No problem! Thanks again for your contribution! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants