Skip to content

Commit

Permalink
fix: Handle one edge-case in reader & some other improvements (#223)
Browse files Browse the repository at this point in the history
Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam authored Sep 25, 2024
1 parent 54a8e9c commit 7c6c2fa
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 135 deletions.
1 change: 1 addition & 0 deletions .idea/appInsightsSettings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 1 addition & 7 deletions app/src/main/java/com/starry/myne/di/MainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package com.starry.myne.di
import android.content.Context
import com.starry.myne.api.BookAPI
import com.starry.myne.database.MyneDatabase
import com.starry.myne.epub.EpubCache
import com.starry.myne.epub.EpubParser
import com.starry.myne.helpers.PreferenceUtil
import com.starry.myne.helpers.book.BookDownloader
Expand Down Expand Up @@ -65,12 +64,7 @@ class MainModule {

@Singleton
@Provides
fun provideEpubcahe(@ApplicationContext context: Context) = EpubCache(context)

@Singleton
@Provides
fun provideEpubParser(@ApplicationContext context: Context, epubCache: EpubCache) =
EpubParser(context, epubCache)
fun provideEpubParser(@ApplicationContext context: Context) = EpubParser(context)

@Provides
@Singleton
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/starry/myne/epub/EpubCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,22 @@ class EpubCache(private val context: Context) {
}
}

/**
* Removes a book from the cache.
*
* @param filepath The path to the book file.
*/
fun remove(filepath: String): Boolean {
Log.d(TAG, "Removing book from cache: $filepath")
val fileName = File(filepath).nameWithoutExtension
val bookFile = File(getPath(), "$fileName.json")
return if (bookFile.exists()) {
bookFile.delete()
} else {
false
}
}

/**
* Checks if a book is cached.
*
Expand Down
43 changes: 34 additions & 9 deletions app/src/main/java/com/starry/myne/epub/EpubParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ import org.w3c.dom.Node
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.concurrent.ThreadLocalRandom
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream


/**
* Parses an EPUB file and creates an [EpubBook] object.
*/
class EpubParser(private val context: Context, private val epubCache: EpubCache) {
class EpubParser(private val context: Context) {

/**
* Represents an EPUB document.
Expand Down Expand Up @@ -104,6 +105,8 @@ class EpubParser(private val context: Context, private val epubCache: EpubCache)
const val TAG = "EpubParser"
}

private val epubCache = EpubCache(context)

/**
* Creates an [EpubBook] object from an EPUB file.
*
Expand Down Expand Up @@ -154,6 +157,15 @@ class EpubParser(private val context: Context, private val epubCache: EpubCache)
return epubCache.isCached(filePath)
}

/**
* Removes an EPUB book from the cache.
*
* @param filePath The file path of the EPUB file.
*/
fun removeBookFromCache(filePath: String): Boolean {
return epubCache.remove(filePath)
}

/**
* Parses and creates an [EpubBook] object from the EPUB files and document.
* This function is called from [createEpubBook] and [createEpubBook].
Expand Down Expand Up @@ -191,9 +203,9 @@ class EpubParser(private val context: Context, private val epubCache: EpubCache)
}

// Determine the method of parsing chapters based on the presence of ToC and
// the shouldUseToc flag. If tocNavPoints is not null or empty and shouldUseToc
// is true, use the ToC file for parsing. Otherwise, parse using the spine.
val chapters = if (!tocNavPoints.isNullOrEmpty() && shouldUseToc) {
// the shouldUseToc flag. We also check if the ToC file has more than one navPoint
// to ensure that it is a valid ToC file.
val chapters = if (shouldUseToc && !tocNavPoints.isNullOrEmpty() && tocNavPoints.size > 1) {
Log.d(TAG, "Parsing based on ToC file")
parseUsingTocFile(tocNavPoints, files, hrefRootPath, document, manifestItems)
} else {
Expand Down Expand Up @@ -231,17 +243,17 @@ class EpubParser(private val context: Context, private val epubCache: EpubCache)
val epubDocument = try {
createEpubDocument(files)
} catch (exc: EpubParserException) {
// In some rare cases, the ZipInputStream does not contains / fails to read all of the files
// In some rare cases, the ZipInputStream does not contain or fails to read all of the files
// required to parse the EPUB archive, even though the zip file itself contains them.
// In such cases, retry parsing the EPUB file by directly using the ZipFile API.
// Since ZipFile requires a file path, we need to create a temporary file from the input stream.
//
// Reasons for this issue are unknown and may be related to how the EPUB file is compressed
// i.e. weather it is missing some metadata or file/folder entry in it's header or how the
// The reasons for this issue are unknown and may be related to how the EPUB file is compressed,
// i.e., whether it is missing some metadata or file/folder entry in its header, or how the
// ZipInputStream reads the file.
//
// If someone knows the exact reason for this issue, or have dealt with it before, please
// let me know or feel free to create a PR with a better solution.
// If anyone knows the exact reason for this issue or has dealt with it before, please
// let me know, or feel free to create a PR with a better solution.
if (exc.message == "META-INF/container.xml file missing"
|| exc.message == ".opf file missing"
) {
Expand Down Expand Up @@ -413,6 +425,17 @@ class EpubParser(private val context: Context, private val epubCache: EpubCache)
return navPoints
}

/**
* Generate a unique ID for a chapter.
*
* @return The generated ID.
*/
private fun generateId(): String {
val timestamp = System.currentTimeMillis()
val randomSuffix = ThreadLocalRandom.current().nextInt(1000, 9999)
return "$timestamp-$randomSuffix"
}

/**
* Parse the EPUB file using the table of contents (ToC) file.
* This method is called from [parseAndCreateEbook].
Expand Down Expand Up @@ -468,6 +491,7 @@ class EpubParser(private val context: Context, private val epubCache: EpubCache)
if (res != null) {
listOf(
EpubChapter(
chapterId = generateId(),
absPath = chapterSrc,
title = title?.takeIf { it.isNotEmpty() } ?: "Chapter $index",
body = res.body
Expand Down Expand Up @@ -548,6 +572,7 @@ class EpubParser(private val context: Context, private val epubCache: EpubCache)
it.chapterIndex
}.map { (index, list) ->
EpubChapter(
chapterId = generateId(),
absPath = list.first().url,
title = list.first().title?.takeIf { it.isNotBlank() } ?: "Chapter $index",
body = list.joinToString("\n\n") { it.body }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class EpubChapter(
val chapterId: String,
val absPath: String,
val title: String,
val body: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class LibraryViewModel @Inject constructor(
val showOnboardingTapTargets: State<Boolean> = _showOnboardingTapTargets

fun deleteItemFromDB(item: LibraryItem) {
epubParser.removeBookFromCache(item.filePath)
viewModelScope.launch(Dispatchers.IO) { libraryDao.delete(item) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.core.view.WindowCompat
Expand All @@ -36,7 +37,7 @@ import androidx.lifecycle.ViewModelProvider
import com.starry.myne.R
import com.starry.myne.helpers.Constants
import com.starry.myne.helpers.toToast
import com.starry.myne.ui.screens.reader.composables.ReaderContent
import com.starry.myne.ui.screens.reader.composables.ChaptersContent
import com.starry.myne.ui.screens.reader.composables.ReaderScreen
import com.starry.myne.ui.screens.reader.viewmodels.ReaderViewModel
import com.starry.myne.ui.screens.settings.viewmodels.SettingsViewModel
Expand Down Expand Up @@ -80,40 +81,39 @@ class ReaderActivity : AppCompatActivity() {
// Set UI contents.
setContent {
MyneTheme(settingsViewModel = settingsViewModel) {

val lazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()

// Handle intent and load epub book.
val intentData = handleIntent(intent = intent,
viewModel = viewModel,
contentResolver = contentResolver,
scrollToPosition = { index, offset ->
coroutineScope.launch {
lazyListState.scrollToItem(index, offset)
}
},
onError = {
getString(R.string.error).toToast(this)
finish()
})
val intentData = remember {
handleIntent(intent = intent,
viewModel = viewModel,
contentResolver = contentResolver,
scrollToPosition = { index, offset ->
coroutineScope.launch {
lazyListState.scrollToItem(index, offset)
}
},
onError = {
getString(R.string.error).toToast(this)
finish()
})
}

ReaderScreen(
viewModel = viewModel,
lazyListState = lazyListState,
readerContent = {
onScrollToChapter = { lazyListState.scrollToItem(it) },
chaptersContent = {
LaunchedEffect(lazyListState) {
snapshotFlow {
lazyListState.firstVisibleItemScrollOffset
}.collect { visibleChapterOffset ->
// fetch last visible chapter position and offset.
// Get the currently visible chapter index.
val visibleChapterIdx = lazyListState.firstVisibleItemIndex
// Set currently visible chapter & index.
viewModel.setVisibleChapterIndex(visibleChapterIdx)
viewModel.setChapterScrollPercent(
calculateChapterPercentage(lazyListState)
)

// If book was not opened from external epub file, update the
// reading progress into the database.
if (!intentData.isExternalFile) {
Expand All @@ -129,13 +129,14 @@ class ReaderActivity : AppCompatActivity() {
}

// Reader content lazy column.
ReaderContent(
ChaptersContent(
state = viewModel.state,
lazyListState = lazyListState,
onToggleReaderMenu = {
viewModel.toggleReaderMenu()
toggleSystemBars(viewModel.state.showReaderMenu)
})
}
)
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import com.starry.myne.ui.theme.pacificoFont


@Composable
fun ReaderContent(
fun ChaptersContent(
state: ReaderScreenState,
lazyListState: LazyListState,
onToggleReaderMenu: () -> Unit
Expand All @@ -72,7 +72,7 @@ fun ReaderContent(
) {
items(
count = state.epubBook!!.chapters.size,
key = { index -> state.epubBook.chapters[index].hashCode() }
key = { index -> state.epubBook.chapters[index].chapterId }
) { index ->
val chapter = state.epubBook.chapters[index]
ChapterLazyItemItem(
Expand Down Expand Up @@ -194,9 +194,7 @@ private fun ChapterLazyItemItem(
onClick()
}
}

}
// .noRippleClickable { onClick() }
) {
Text(
modifier = Modifier.padding(start = 12.dp, end = 4.dp, top = 10.dp),
Expand Down
Loading

0 comments on commit 7c6c2fa

Please sign in to comment.