diff --git a/.idea/other.xml b/.idea/other.xml deleted file mode 100644 index 94c96f63..00000000 --- a/.idea/other.xml +++ /dev/null @@ -1,318 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index e8e54a7e..5ab5cc2c 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,12 @@ It is easiest to make a translation using the Android Studio XML editor, but you - [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - Data objects that notify views when the underlying database changes. - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores UI-related data that isn't destroyed on UI changes. - [OkHttp3](https://square.github.io/okhttp/) - OkHttp is an HTTP client for Android that’s efficient by default. -- [Gson](https://github.com/google/gson) - A Java serialization/deserialization library to convert Java Objects into JSON and back. +- [Kotlinx.serialization](https://kotlinlang.org/docs/serialization.html) - Provides sets of libraries for various serialization formats – JSON, CBOR, protocol buffers, and others. - [Jsoup](https://jsoup.org) - Jsoup is a Java library for working with HTML. It provides a convenient API for extracting and manipulating data, using the HTML5 DOM methods and CSS selectors. - [Coil](https://coil-kt.github.io/coil/compose/) - An image loading library for Android backed by Kotlin Coroutines. - [Dagger-Hilt](https://dagger.dev/hilt/) For [Dependency injection (DI)](https://developer.android.com/training/dependency-injection) - [Room database](https://developer.android.com/jetpack/androidx/releases/room) - Persistence library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite. +- [Lottie](https://airbnb.design/lottie) - Lottie is an Android, iOS and React Native library that renders After Effects animations in real time. ------ diff --git a/app/build.gradle b/app/build.gradle index 72d42d0f..2db191b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'org.jetbrains.kotlin.plugin.compose' id 'dagger.hilt.android.plugin' id 'com.google.devtools.ksp' + id 'org.jetbrains.kotlin.plugin.serialization' id "com.mikepenz.aboutlibraries.plugin" version "11.1.3" } @@ -92,15 +93,15 @@ aboutLibraries { dependencies { - def composeBom = platform('androidx.compose:compose-bom:2024.09.01') + def composeBom = platform('androidx.compose:compose-bom:2024.09.02') implementation composeBom androidTestImplementation composeBom // Android core components. implementation 'androidx.core:core-ktx:1.13.1' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.5' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6' implementation 'androidx.activity:activity-compose:1.9.2' - implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6" implementation "androidx.navigation:navigation-compose:2.8.0" // Jetpack compose. implementation "androidx.compose.ui:ui" @@ -110,13 +111,13 @@ dependencies { implementation "androidx.compose.runtime:runtime-livedata" implementation "androidx.compose.material3:material3" // Material icons. - implementation 'androidx.compose.material:material-icons-extended:1.7.1' + implementation 'androidx.compose.material:material-icons-extended:1.7.2' // Material theme for main activity. implementation 'com.google.android.material:material:1.12.0' // Android 12+ splash API. implementation 'androidx.core:core-splashscreen:1.0.1' - // Gson JSON library. - implementation 'com.google.code.gson:gson:2.10.1' + // KotlinX Serialization library. + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" // OkHttp library. implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" diff --git a/app/src/main/java/com/starry/myne/api/BookAPI.kt b/app/src/main/java/com/starry/myne/api/BookAPI.kt index 12708987..cfbcb9ac 100644 --- a/app/src/main/java/com/starry/myne/api/BookAPI.kt +++ b/app/src/main/java/com/starry/myne/api/BookAPI.kt @@ -17,13 +17,13 @@ package com.starry.myne.api import android.content.Context -import com.google.gson.Gson import com.starry.myne.BuildConfig import com.starry.myne.api.models.BookSet import com.starry.myne.api.models.ExtraInfo import com.starry.myne.helpers.book.BookLanguage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import okhttp3.Cache import okhttp3.Call import okhttp3.Callback @@ -74,8 +74,7 @@ class BookAPI(context: Context) { okHttpBuilder.build() } - private val gsonClient = Gson() // Gson client for parsing JSON responses. - + private val json = Json { ignoreUnknownKeys = true } /** * This function fetches all the books from the API. @@ -150,7 +149,10 @@ class BookAPI(context: Context) { response.use { continuation.resume( Result.success( - gsonClient.fromJson(response.body!!.string(), BookSet::class.java) + json.decodeFromString( + BookSet.serializer(), + response.body!!.string() + ) ) ) } diff --git a/app/src/main/java/com/starry/myne/api/models/Author.kt b/app/src/main/java/com/starry/myne/api/models/Author.kt index 47ccc3f4..2e3a3372 100644 --- a/app/src/main/java/com/starry/myne/api/models/Author.kt +++ b/app/src/main/java/com/starry/myne/api/models/Author.kt @@ -16,16 +16,17 @@ package com.starry.myne.api.models - import androidx.annotation.Keep -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class Author( - @SerializedName("name") + @SerialName("birth_year") + val birthYear: Int? = null, + @SerialName("death_year") + val deathYear: Int? = null, + @SerialName("name") val name: String = "N/A", - @SerializedName("birth_year") - val birthYear: Int, - @SerializedName("death_year") - val deathYear: Int ) \ No newline at end of file diff --git a/app/src/main/java/com/starry/myne/api/models/Book.kt b/app/src/main/java/com/starry/myne/api/models/Book.kt index ecde614f..e49d5dec 100644 --- a/app/src/main/java/com/starry/myne/api/models/Book.kt +++ b/app/src/main/java/com/starry/myne/api/models/Book.kt @@ -16,32 +16,33 @@ package com.starry.myne.api.models - import androidx.annotation.Keep -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class Book( - @SerializedName("id") - val id: Int, - @SerializedName("title") - val title: String, - @SerializedName("authors") + @SerialName("authors") val authors: List, - @SerializedName("translators") - val translators: List, - @SerializedName("subjects") - val subjects: List, - @SerializedName("bookshelves") + @SerialName("bookshelves") val bookshelves: List, - @SerializedName("languages") - val languages: List, - @SerializedName("copyright") + @SerialName("copyright") val copyright: Boolean, - @SerializedName("media_type") - val mediaType: String, - @SerializedName("formats") + @SerialName("download_count") + val downloadCount: Int, + @SerialName("formats") val formats: Formats, - @SerializedName("download_count") - val downloadCount: Long + @SerialName("id") + val id: Int, + @SerialName("languages") + val languages: List, + @SerialName("media_type") + val mediaType: String, + @SerialName("subjects") + val subjects: List, + @SerialName("title") + val title: String, + @SerialName("translators") + val translators: List ) \ No newline at end of file diff --git a/app/src/main/java/com/starry/myne/api/models/BookSet.kt b/app/src/main/java/com/starry/myne/api/models/BookSet.kt index 4c033b28..8ed41b6c 100644 --- a/app/src/main/java/com/starry/myne/api/models/BookSet.kt +++ b/app/src/main/java/com/starry/myne/api/models/BookSet.kt @@ -16,18 +16,19 @@ package com.starry.myne.api.models - import androidx.annotation.Keep -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class BookSet( - @SerializedName("count") - val count: Int, - @SerializedName("next") - val next: String?, - @SerializedName("previous") - val previous: String?, - @SerializedName("results") - val books: List + @SerialName("count") + val count: Int = 0, + @SerialName("next") + val next: String? = null, + @SerialName("previous") + val previous: String? = null, + @SerialName("results") + val books: List = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/com/starry/myne/api/models/Formats.kt b/app/src/main/java/com/starry/myne/api/models/Formats.kt index 9b6e808f..738b5dd4 100644 --- a/app/src/main/java/com/starry/myne/api/models/Formats.kt +++ b/app/src/main/java/com/starry/myne/api/models/Formats.kt @@ -16,34 +16,33 @@ package com.starry.myne.api.models - import androidx.annotation.Keep -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class Formats( - @SerializedName("text/plain") - val textplain: String, - @SerializedName("application/x-mobipocket-ebook") - val applicationxMobipocketEbook: String?, - @SerializedName("text/html") - val texthtml: String?, - @SerializedName("application/octet-stream") - val applicationoctetStream: String?, - @SerializedName("text/plain; charset=us-ascii") - val textplainCharsetusAscii: String?, - @SerializedName("application/epub+zip") - val applicationepubzip: String?, - @SerializedName("image/jpeg") - val imagejpeg: String?, - @SerializedName("application/rdf+xml") - val applicationrdfxml: String?, - @SerializedName("text/html; charset=iso-8859-1") - val texthtmlCharsetiso88591: String?, - @SerializedName("text/html; charset=utf-8") - val texthtmlCharsetutf8: String?, - @SerializedName("text/plain; charset=utf-8") - val textplainCharsetutf8: String?, - @SerializedName("text/plain; charset=iso-8859-1") - val textplainCharsetiso88591: String? + @SerialName("application/epub+zip") + val applicationepubzip: String? = null, + @SerialName("application/octet-stream") + val applicationoctetStream: String? = null, + @SerialName("application/rdf+xml") + val applicationrdfxml: String? = null, + @SerialName("application/x-mobipocket-ebook") + val applicationxMobipocketEbook: String? = null, + @SerialName("image/jpeg") + val imagejpeg: String? = null, + @SerialName("text/html") + val texthtml: String? = null, + @SerialName("text/html; charset=iso-8859-1") + val texthtmlCharsetiso88591: String? = null, + @SerialName("text/html; charset=utf-8") + val texthtmlCharsetutf8: String? = null, + @SerialName("text/plain; charset=iso-8859-1") + val textplainCharsetiso88591: String? = null, + @SerialName("text/plain; charset=us-ascii") + val textplainCharsetusAscii: String? = null, + @SerialName("text/plain; charset=utf-8") + val textplainCharsetutf8: String? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/starry/myne/api/models/Translator.kt b/app/src/main/java/com/starry/myne/api/models/Translator.kt index bc8bba17..603ca217 100644 --- a/app/src/main/java/com/starry/myne/api/models/Translator.kt +++ b/app/src/main/java/com/starry/myne/api/models/Translator.kt @@ -16,16 +16,17 @@ package com.starry.myne.api.models - import androidx.annotation.Keep -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class Translator( - @SerializedName("name") + @SerialName("birth_year") + val birthYear: Int? = null, + @SerialName("death_year") + val deathYear: Int? = null, + @SerialName("name") val name: String = "N/A", - @SerializedName("birth_year") - val birthYear: Int, - @SerializedName("death_year") - val deathYear: Int ) \ No newline at end of file diff --git a/app/src/main/java/com/starry/myne/di/MainModule.kt b/app/src/main/java/com/starry/myne/di/MainModule.kt index 518985e1..c15d3bea 100644 --- a/app/src/main/java/com/starry/myne/di/MainModule.kt +++ b/app/src/main/java/com/starry/myne/di/MainModule.kt @@ -20,6 +20,7 @@ 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 @@ -64,7 +65,12 @@ class MainModule { @Singleton @Provides - fun provideEpubParser(@ApplicationContext context: Context) = EpubParser(context) + fun provideEpubcahe(@ApplicationContext context: Context) = EpubCache(context) + + @Singleton + @Provides + fun provideEpubParser(@ApplicationContext context: Context, epubCache: EpubCache) = + EpubParser(context, epubCache) @Provides @Singleton diff --git a/app/src/main/java/com/starry/myne/epub/BitmapSerializer.kt b/app/src/main/java/com/starry/myne/epub/BitmapSerializer.kt new file mode 100644 index 00000000..54e488a9 --- /dev/null +++ b/app/src/main/java/com/starry/myne/epub/BitmapSerializer.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) [2022 - Present] Stɑrry Shivɑm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.starry.myne.epub + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.io.ByteArrayOutputStream + +object BitmapSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("Bitmap", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Bitmap) { + val stream = ByteArrayOutputStream() + value.compress(Bitmap.CompressFormat.PNG, 100, stream) + val byteArray = stream.toByteArray() + encoder.encodeString( + android.util.Base64.encodeToString( + byteArray, android.util.Base64.DEFAULT + ) + ) + } + + override fun deserialize(decoder: Decoder): Bitmap { + val byteArray = + android.util.Base64.decode(decoder.decodeString(), android.util.Base64.DEFAULT) + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + } +} diff --git a/app/src/main/java/com/starry/myne/epub/EpubCache.kt b/app/src/main/java/com/starry/myne/epub/EpubCache.kt new file mode 100644 index 00000000..bf660837 --- /dev/null +++ b/app/src/main/java/com/starry/myne/epub/EpubCache.kt @@ -0,0 +1,140 @@ +/** + * Copyright (c) [2022 - Present] Stɑrry Shivɑm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.starry.myne.epub + +import android.content.Context +import android.util.Log +import com.starry.myne.epub.models.EpubBook +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File + +/** + * A cache for storing epub books. + * + * @param context The context. + */ +class EpubCache(private val context: Context) { + + companion object { + private const val TAG = "EpubCache" + private const val EPUB_CACHE = "epub_cache" + private const val CACHE_VERSION_FILE = "cache_version" + + // Increment this if the cache format changes. + private const val EPUB_CACHE_VERSION = 1 + } + + init { + create() + checkCacheVersion() + } + + private fun getPath(): String { + return context.cacheDir.absolutePath + File.separator + EPUB_CACHE + } + + private fun getVersionFilePath(): String { + return getPath() + File.separator + CACHE_VERSION_FILE + } + + private fun checkCacheVersion() { + Log.d(TAG, "Checking cache version") + val versionFile = File(getVersionFilePath()) + if (versionFile.exists()) { + val cachedVersion = versionFile.readText().toIntOrNull() + if (cachedVersion != EPUB_CACHE_VERSION) { + Log.d(TAG, "Cache version mismatch, clearing cache") + clear() + create() + saveCacheVersion() + } + } else { + Log.d(TAG, "Cache version not found, saving cache version") + saveCacheVersion() + } + } + + private fun saveCacheVersion() { + Log.d(TAG, "Saving cache version") + val versionFile = File(getVersionFilePath()) + versionFile.writeText(EPUB_CACHE_VERSION.toString()) + } + + private fun create() { + val cacheDir = File(getPath()) + if (!cacheDir.exists()) { + Log.d(TAG, "Creating cache directory") + cacheDir.mkdirs() + } + } + + private fun clear() { + val cacheDir = File(getPath()) + if (cacheDir.exists()) { + Log.d(TAG, "Clearing cache directory") + cacheDir.deleteRecursively() + } + } + + /** + * Adds a book to the cache. + * + * @param book The book to add. + * @param filepath The path to the book file. + */ + fun put(book: EpubBook, filepath: String) { + Log.d(TAG, "Inserting book into cache: ${book.title}") + val fileName = File(filepath).nameWithoutExtension + val bookFile = File(getPath(), "$fileName.json") + val jsonString = Json.encodeToString(book) + bookFile.writeText(jsonString) + } + + /** + * Gets a book from the cache. + * If the book is not cached, null is returned. + * + * @param filepath The path to the book file. + * @return The book if it is cached, null otherwise. + */ + fun get(filepath: String): EpubBook? { + Log.d(TAG, "Getting book from cache: $filepath") + val fileName = File(filepath).nameWithoutExtension + val bookFile = File(getPath(), "$fileName.json") + return if (bookFile.exists()) { + Log.d(TAG, "Book found in cache: $filepath") + val jsonString = bookFile.readText() + Json.decodeFromString(jsonString) + } else { + Log.d(TAG, "Book not found in cache: $filepath") + null + } + } + + /** + * Checks if a book is cached. + * + * @param filepath The path to the book file. + * @return True if the book is cached, false otherwise. + */ + fun isCached(filepath: String): Boolean { + val fileName = File(filepath).nameWithoutExtension + val bookFile = File(getPath(), "$fileName.json") + return bookFile.exists() + } +} diff --git a/app/src/main/java/com/starry/myne/epub/EpubParser.kt b/app/src/main/java/com/starry/myne/epub/EpubParser.kt index bc561c39..04486b6e 100644 --- a/app/src/main/java/com/starry/myne/epub/EpubParser.kt +++ b/app/src/main/java/com/starry/myne/epub/EpubParser.kt @@ -38,7 +38,7 @@ import java.util.zip.ZipInputStream /** * Parses an EPUB file and creates an [EpubBook] object. */ -class EpubParser(private val context: Context) { +class EpubParser(private val context: Context, private val epubCache: EpubCache) { /** * Represents an EPUB document. @@ -113,10 +113,17 @@ class EpubParser(private val context: Context) { */ suspend fun createEpubBook(filePath: String, shouldUseToc: Boolean = true): EpubBook { return withContext(Dispatchers.IO) { + val epubBook = epubCache.get(filePath) + if (epubBook != null) { + Log.d(TAG, "EpubBook found in cache") + return@withContext epubBook + } Log.d(TAG, "Parsing EPUB file: $filePath") val files = getZipFilesFromFile(filePath) val document = createEpubDocument(files) - parseAndCreateEbook(files, document, shouldUseToc) + val book = parseAndCreateEbook(files, document, shouldUseToc) + epubCache.put(book, filePath) + return@withContext book } } @@ -137,6 +144,15 @@ class EpubParser(private val context: Context) { } } + /** + * Checks if an EPUB book is cached. + * + * @param filePath The file path of the EPUB file. + * @return True if the book is cached, false otherwise. + */ + fun isBookCached(filePath: String): Boolean { + return epubCache.isCached(filePath) + } /** * Parses and creates an [EpubBook] object from the EPUB files and document. diff --git a/app/src/main/java/com/starry/myne/epub/models/EpubBook.kt b/app/src/main/java/com/starry/myne/epub/models/EpubBook.kt index 786849eb..8eaa2397 100644 --- a/app/src/main/java/com/starry/myne/epub/models/EpubBook.kt +++ b/app/src/main/java/com/starry/myne/epub/models/EpubBook.kt @@ -18,6 +18,8 @@ package com.starry.myne.epub.models import android.graphics.Bitmap +import com.starry.myne.epub.BitmapSerializer +import kotlinx.serialization.Serializable /** * Represents an epub book. @@ -30,11 +32,13 @@ import android.graphics.Bitmap * @param chapters The list of chapters in the book. * @param images The list of images in the book. */ +@Serializable data class EpubBook( val fileName: String, val title: String, val author: String, val language: String, + @Serializable(with = BitmapSerializer::class) val coverImage: Bitmap?, val chapters: List, val images: List diff --git a/app/src/main/java/com/starry/myne/epub/models/EpubChapter.kt b/app/src/main/java/com/starry/myne/epub/models/EpubChapter.kt index b23fbfb9..de4d6226 100644 --- a/app/src/main/java/com/starry/myne/epub/models/EpubChapter.kt +++ b/app/src/main/java/com/starry/myne/epub/models/EpubChapter.kt @@ -17,6 +17,8 @@ package com.starry.myne.epub.models +import kotlinx.serialization.Serializable + /** * Represents a chapter in an epub book. * @@ -24,6 +26,7 @@ package com.starry.myne.epub.models * @param title The title of the chapter. * @param body The body of the chapter. */ +@Serializable data class EpubChapter( val absPath: String, val title: String, diff --git a/app/src/main/java/com/starry/myne/epub/models/EpubImage.kt b/app/src/main/java/com/starry/myne/epub/models/EpubImage.kt index 225bf55a..b16015ea 100644 --- a/app/src/main/java/com/starry/myne/epub/models/EpubImage.kt +++ b/app/src/main/java/com/starry/myne/epub/models/EpubImage.kt @@ -17,12 +17,15 @@ package com.starry.myne.epub.models +import kotlinx.serialization.Serializable + /** * Represents an image in an epub book. * * @param absPath The absolute path of the image. * @param image The image data. */ +@Serializable data class EpubImage(val absPath: String, val image: ByteArray) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/app/src/main/java/com/starry/myne/ui/common/placeholder/PlaceHolder.kt b/app/src/main/java/com/starry/myne/ui/common/placeholder/PlaceHolder.kt index a1b338b0..f0f127e9 100644 --- a/app/src/main/java/com/starry/myne/ui/common/placeholder/PlaceHolder.kt +++ b/app/src/main/java/com/starry/myne/ui/common/placeholder/PlaceHolder.kt @@ -9,9 +9,9 @@ import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -142,7 +142,7 @@ fun Modifier.placeholder( val transitionState = remember { MutableTransitionState(visible) }.apply { targetState = visible } - val transition = updateTransition(transitionState, "placeholder_crossfade") + val transition = rememberTransition(transitionState, "placeholder_crossfade") val placeholderAlpha by transition.animateFloat( transitionSpec = placeholderFadeTransitionSpec, diff --git a/app/src/main/java/com/starry/myne/ui/screens/categories/viewmodels/CategoryViewModel.kt b/app/src/main/java/com/starry/myne/ui/screens/categories/viewmodels/CategoryViewModel.kt index 45fba071..8a6a9e12 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/categories/viewmodels/CategoryViewModel.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/categories/viewmodels/CategoryViewModel.kt @@ -83,19 +83,7 @@ class CategoryViewModel @Inject constructor( state = state.copy(error = it?.localizedMessage ?: Constants.UNKNOWN_ERR) }, onSuccess = { bookSet, newPage -> - - /** - * usually bookSet.books is not nullable and API simply returns empty list - * when browsing books all books (i.e. without passing language parameter) - * however, when browsing by language it returns a response which looks like - * this: {"detail": "Invalid page."}. Hence the [BookSet] attributes become - * null in this case and can cause crashes. - */ - @Suppress("SENSELESS_COMPARISON") val books = if (bookSet.books != null) { - bookSet.books.filter { it.formats.applicationepubzip != null } - } else { - emptyList() - } + val books = bookSet.books.filter { it.formats.applicationepubzip != null } state = state.copy( items = (state.items + books), page = newPage, diff --git a/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt b/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt index e804dc9f..89c276fb 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.starry.myne.api.BookAPI import com.starry.myne.api.models.Book -import com.starry.myne.api.models.BookSet import com.starry.myne.helpers.Constants import com.starry.myne.helpers.NetworkObserver import com.starry.myne.helpers.Paginator @@ -94,24 +93,16 @@ class HomeViewModel @Inject constructor( }, onError = { allBooksState = allBooksState.copy(error = it?.localizedMessage ?: Constants.UNKNOWN_ERR) }, onSuccess = { bookSet, newPage -> - /** - * usually bookSet.books is not nullable and API simply returns empty list - * when browsing books all books (i.e. without passing language parameter) - * however, when browsing by language it returns a response which looks like - * this: {"detail": "Invalid page."}. Hence the [BookSet] attributes become - * null in this case and can cause crashes. - */ - @Suppress("SENSELESS_COMPARISON") val books = if (bookSet.books != null) { + + val books = run { val books = bookSet.books.filter { it.formats.applicationepubzip != null } as ArrayList - // Remove the book with id 1513 + // Ignore ... val index = books.indexOfFirst { it.id == 1513 } if (index != -1) { books.removeAt(index) } books // return the list of books - } else { - ArrayList() } allBooksState = allBooksState.copy( diff --git a/app/src/main/java/com/starry/myne/ui/screens/library/composables/LibraryScreen.kt b/app/src/main/java/com/starry/myne/ui/screens/library/composables/LibraryScreen.kt index d8d5cf4e..5fa81a26 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/library/composables/LibraryScreen.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/library/composables/LibraryScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.animation.core.keyframes import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -287,7 +286,6 @@ fun LibraryScreen(navController: NavController) { } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun LibraryContents( viewModel: LibraryViewModel, @@ -341,7 +339,7 @@ private fun LibraryContents( val item = libraryItems[i] if (item.fileExist()) { LibraryLazyItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), item = item, snackBarHostState = snackBarHostState, navController = navController, diff --git a/app/src/main/java/com/starry/myne/ui/screens/reader/composables/ReaderScreen.kt b/app/src/main/java/com/starry/myne/ui/screens/reader/composables/ReaderScreen.kt index d092de30..2db78518 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/reader/composables/ReaderScreen.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/reader/composables/ReaderScreen.kt @@ -277,7 +277,9 @@ fun ReaderScreen( .padding(bottom = 65.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + if (viewModel.state.shouldShowLoader) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } } } else { readerContent(paddingValues) diff --git a/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderDetailViewModel.kt b/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderDetailViewModel.kt index 84c67b89..64d6eaef 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderDetailViewModel.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderDetailViewModel.kt @@ -96,7 +96,7 @@ class ReaderDetailViewModel @Inject constructor( authors = libraryItem.authors, epubBook = epubBook ) - delay(500) // Add delay to avoid flickering. + delay(450) // Add delay to avoid flickering. state = state.copy(isLoading = false, ebookData = ebookData) } } diff --git a/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderViewModel.kt b/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderViewModel.kt index 0e28e0fe..f515468b 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderViewModel.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/reader/viewmodels/ReaderViewModel.kt @@ -42,6 +42,7 @@ import javax.inject.Inject data class ReaderScreenState( val isLoading: Boolean = true, + val shouldShowLoader: Boolean = false, val showReaderMenu: Boolean = false, val fontSize: Int = 18, val fontFamily: ReaderFont = ReaderFont.System, @@ -73,6 +74,7 @@ class ReaderViewModel @Inject constructor( fun loadEpubBook(libraryItemId: Int, onLoaded: (ReaderScreenState) -> Unit) { viewModelScope.launch(Dispatchers.IO) { val libraryItem = libraryDao.getItemById(libraryItemId) + state = state.copy(shouldShowLoader = !epubParser.isBookCached(libraryItem!!.filePath)) val readerData = readerDao.getReaderData(libraryItemId) // parse and create epub book var epubBook = epubParser.createEpubBook(libraryItem!!.filePath) @@ -85,7 +87,9 @@ class ReaderViewModel @Inject constructor( state = state.copy(epubBook = epubBook, readerData = readerData) onLoaded(state) // Added some delay to avoid choppy animation. - delay(350L) + if (state.shouldShowLoader) { + delay(200L) + } state = state.copy(isLoading = false) } } diff --git a/app/src/test/java/com/starry/myne/BookUtilsTest.kt b/app/src/test/java/com/starry/myne/BookUtilsTest.kt index 9efc61e7..b032acf7 100644 --- a/app/src/test/java/com/starry/myne/BookUtilsTest.kt +++ b/app/src/test/java/com/starry/myne/BookUtilsTest.kt @@ -26,14 +26,14 @@ import org.junit.Test class BookUtilsTest { @Test fun `getAuthorsAsString returns expected string with one author`() = runTest { - val author = Author("Dostoyevsky, Fyodor", 0, 0) + val author = Author(name = "Dostoyevsky, Fyodor", birthYear = 0, deathYear = 0) assertThat(BookUtils.getAuthorsAsString(listOf(author))).isEqualTo("Fyodor Dostoyevsky") } @Test fun `getAuthorsAsString returns expected string with multiple authors`() = runTest { - val author1 = Author("Dostoyevsky, Fyodor", 0, 0) - val author2 = Author("Orwell, George", 0, 0) + val author1 = Author(name = "Dostoyevsky, Fyodor", birthYear = 0, deathYear = 0) + val author2 = Author(name = "Orwell, George", birthYear = 0, deathYear = 0) assertThat( BookUtils.getAuthorsAsString( listOf( diff --git a/app/src/test/java/com/starry/myne/EpubParserTest.kt b/app/src/test/java/com/starry/myne/EpubParserTest.kt index 991e480c..b7a64d53 100644 --- a/app/src/test/java/com/starry/myne/EpubParserTest.kt +++ b/app/src/test/java/com/starry/myne/EpubParserTest.kt @@ -18,6 +18,7 @@ package com.starry.myne import android.graphics.Bitmap import com.google.common.truth.Truth.assertThat +import com.starry.myne.epub.EpubCache import com.starry.myne.epub.EpubParser import kotlinx.coroutines.runBlocking import org.junit.Before @@ -43,7 +44,10 @@ class EpubParserTest { @Before fun setup() { - epubParser = EpubParser(RuntimeEnvironment.getApplication()) + epubParser = EpubParser( + RuntimeEnvironment.getApplication(), + EpubCache(RuntimeEnvironment.getApplication()) + ) // Create a sample EPUB file for testing testEpubFile = createSampleEpubFile(hasToc = false) diff --git a/build.gradle b/build.gradle index 22d50d8d..9d480bd0 100644 --- a/build.gradle +++ b/build.gradle @@ -26,4 +26,5 @@ plugins { id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false id 'org.jetbrains.kotlin.plugin.compose' version "$kotlin_version" apply false id 'com.google.devtools.ksp' version '2.0.0-1.0.23' apply false + id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" apply false } \ No newline at end of file