Skip to content

Commit

Permalink
feat: Add ability to import multiple epubs at once (#179)
Browse files Browse the repository at this point in the history
Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam committed Jun 1, 2024
1 parent 6d214cf commit 6f206ed
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ import kotlinx.coroutines.launch
import me.saket.swipe.SwipeAction
import me.saket.swipe.SwipeableActionsBox
import java.io.File
import java.io.FileInputStream


@OptIn(ExperimentalMaterial3Api::class)
Expand All @@ -133,35 +132,38 @@ fun LibraryScreen(navController: NavController) {

val showImportDialog = remember { mutableStateOf(false) }
val importBookLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let {
(context as MainActivity).contentResolver.openInputStream(uri)?.let { ips ->
showImportDialog.value = true // Show import dialog
viewModel.importBook(
context = context,
fileStream = ips as FileInputStream,
onComplete = {
showImportDialog.value = false
coroutineScope.launch {
snackBarHostState.showSnackbar(
message = context.getString(R.string.epub_imported),
actionLabel = context.getString(R.string.ok),
duration = SnackbarDuration.Short
)
}
},
onError = {
showImportDialog.value = false
coroutineScope.launch {
snackBarHostState.showSnackbar(
message = context.getString(R.string.error),
actionLabel = context.getString(R.string.ok),
duration = SnackbarDuration.Short
)
}
})
rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
// If no files are selected, return.
if (uris.isEmpty()) return@rememberLauncherForActivityResult
// Show dialog to indicate import process.
showImportDialog.value = true
// Start books import.
viewModel.importBooks(
context = context,
fileUris = uris,
onComplete = {
// Hide dialog and show success message.
showImportDialog.value = false
coroutineScope.launch {
snackBarHostState.showSnackbar(
message = context.getString(R.string.epub_imported),
actionLabel = context.getString(R.string.ok),
duration = SnackbarDuration.Short
)
}
},
onError = {
// Hide dialog and show error message.
showImportDialog.value = false
coroutineScope.launch {
snackBarHostState.showSnackbar(
message = context.getString(R.string.error),
actionLabel = context.getString(R.string.ok),
duration = SnackbarDuration.Short
)
}
}
}
)
}

val showTapTargets = remember { mutableStateOf(false) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package com.starry.myne.ui.screens.library.viewmodels

import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -75,43 +77,58 @@ class LibraryViewModel @Inject constructor(
_showOnboardingTapTargets.value = false
}

fun importBook(
fun importBooks(
context: Context,
fileStream: FileInputStream,
fileUris: List<Uri>,
onComplete: () -> Unit,
onError: (Exception) -> Unit
onError: (Throwable) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
try {
fileStream.use { fis ->
val epubBook = epubParser.createEpubBook(fis)
// reset the stream position to 0 so that it can be read again
fis.channel.position(0)
// copy the book to internal storage
val filePath = copyBookToInternalStorage(
context, fis,
BookDownloader.createFileName(epubBook.title)
)
// insert the book into the database
val libraryItem = LibraryItem(
bookId = 0,
title = epubBook.title,
authors = epubBook.author,
filePath = filePath,
createdAt = System.currentTimeMillis(),
isExternalBook = true
)
libraryDao.insert(libraryItem)
delay(800) // delay to prevent from flickering.
withContext(Dispatchers.Main) { onComplete() }
val result = runCatching {
fileUris.forEach { uri ->
context.contentResolver.openInputStream(uri)?.use { fis ->
if (fis !is FileInputStream) {
throw IllegalArgumentException("File input stream is not valid.")
}

val epubBook = epubParser.createEpubBook(fis)
fis.channel.position(0)

val filePath = copyBookToInternalStorage(
context, fis,
BookDownloader.createFileName(epubBook.title)
)

val libraryItem = LibraryItem(
bookId = 0,
title = epubBook.title,
authors = epubBook.author,
filePath = filePath,
createdAt = System.currentTimeMillis(),
isExternalBook = true
)

libraryDao.insert(libraryItem)
}
}

// Add delay here so user can see the import progress bar even if
// the import is very fast instead of just a flicker, improving UX
delay(800)
}

withContext(Dispatchers.Main) {
result.onSuccess {
onComplete()
}.onFailure { exception ->
Log.e("LibraryViewModel", "Error importing book", exception)
onError(exception)
}
} catch (exc: Exception) {
exc.printStackTrace()
withContext(Dispatchers.Main) { onError(exc) }
}
}
}


private suspend fun copyBookToInternalStorage(
context: Context,
fileStream: FileInputStream,
Expand Down

0 comments on commit 6f206ed

Please sign in to comment.