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