Added the com.squareup.retrofit2:converter-scalars converter to Retrofit to parse OPDS network responses.

* Created `LibkiwixBook` to wrap the `Book` class from `libkiwix`, allowing us to set custom values from the database and manage books received from the `OPDS` stream. This wrapper is necessary because the original Book class does not provide setters. We'll use `LibkiwixBook` throughout the codebase.
* Refactored the codebase to replace `LibraryNetworkEntity.Book` with `LibkiwixBook`.
This commit is contained in:
MohitMaliFtechiz 2025-06-04 22:16:09 +05:30
parent 20722fe15b
commit 7a10528d19
25 changed files with 249 additions and 93 deletions

View File

@ -82,7 +82,7 @@ import org.kiwix.kiwixmobile.core.downloader.downloadManager.DEFAULT_INT_VALUE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.FIVE import org.kiwix.kiwixmobile.core.downloader.downloadManager.FIVE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.extensions.calculateSearchMatches import org.kiwix.kiwixmobile.core.extensions.calculateSearchMatches
import org.kiwix.kiwixmobile.core.extensions.registerReceiver import org.kiwix.kiwixmobile.core.extensions.registerReceiver
import org.kiwix.kiwixmobile.core.ui.components.ONE import org.kiwix.kiwixmobile.core.ui.components.ONE
@ -131,6 +131,7 @@ const val MAX_PROGRESS = 100
const val THREE = 3 const val THREE = 3
const val FOUR = 4 const val FOUR = 4
@Suppress("LongParameterList")
class ZimManageViewModel @Inject constructor( class ZimManageViewModel @Inject constructor(
private val downloadDao: DownloadRoomDao, private val downloadDao: DownloadRoomDao,
private val bookDao: NewBookDao, private val bookDao: NewBookDao,
@ -168,7 +169,7 @@ class ZimManageViewModel @Inject constructor(
val onlineLibraryDownloading = MutableStateFlow(false) val onlineLibraryDownloading = MutableStateFlow(false)
val shouldShowWifiOnlyDialog = MutableLiveData<Boolean>() val shouldShowWifiOnlyDialog = MutableLiveData<Boolean>()
val networkStates = MutableLiveData<NetworkState>() val networkStates = MutableLiveData<NetworkState>()
val networkLibrary = MutableSharedFlow<List<org.kiwix.libkiwix.Book>>(replay = 0) val networkLibrary = MutableSharedFlow<List<LibkiwixBook>>(replay = 0)
val requestFileSystemCheck = MutableSharedFlow<Unit>(replay = 0) val requestFileSystemCheck = MutableSharedFlow<Unit>(replay = 0)
val fileSelectActions = MutableSharedFlow<FileSelectActions>() val fileSelectActions = MutableSharedFlow<FileSelectActions>()
private val requestDownloadLibrary = MutableSharedFlow<Unit>( private val requestDownloadLibrary = MutableSharedFlow<Unit>(
@ -391,7 +392,7 @@ class ZimManageViewModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun requestsAndConnectivityChangesToLibraryRequests( private fun requestsAndConnectivityChangesToLibraryRequests(
library: MutableSharedFlow<List<org.kiwix.libkiwix.Book>>, library: MutableSharedFlow<List<LibkiwixBook>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = requestDownloadLibrary.flatMapConcat { ) = requestDownloadLibrary.flatMapConcat {
connectivityBroadcastReceiver.networkStates connectivityBroadcastReceiver.networkStates
@ -442,12 +443,16 @@ class ZimManageViewModel @Inject constructor(
private fun downloadLibraryFlow( private fun downloadLibraryFlow(
kiwixService: KiwixService kiwixService: KiwixService
): Flow<List<org.kiwix.libkiwix.Book>> = flow { ): Flow<List<LibkiwixBook>> = flow {
downloadProgress.postValue(context.getString(R.string.starting_downloading_remote_library)) downloadProgress.postValue(context.getString(R.string.starting_downloading_remote_library))
val response = kiwixService.getLibrary() val response = kiwixService.getLibrary()
downloadProgress.postValue(context.getString(R.string.parsing_remote_library)) downloadProgress.postValue(context.getString(R.string.parsing_remote_library))
onlineLibraryManager.parseOPDSStream(response, KIWIX_OPDS_LIBRARY_URL) val isLibraryParsed = onlineLibraryManager.parseOPDSStream(response, KIWIX_OPDS_LIBRARY_URL)
emit(onlineLibraryManager.getOnlineBooks()) if (isLibraryParsed) {
emit(onlineLibraryManager.getOnlineBooks())
} else {
emit(emptyList())
}
} }
.retry(5) .retry(5)
.catch { e -> .catch { e ->
@ -464,7 +469,7 @@ class ZimManageViewModel @Inject constructor(
private fun updateLibraryItems( private fun updateLibraryItems(
booksFromDao: Flow<List<BookOnDisk>>, booksFromDao: Flow<List<BookOnDisk>>,
downloads: Flow<List<DownloadModel>>, downloads: Flow<List<DownloadModel>>,
library: MutableSharedFlow<List<org.kiwix.libkiwix.Book>>, library: MutableSharedFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>, languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = viewModelScope.launch(dispatcher) { ) = viewModelScope.launch(dispatcher) {
@ -487,14 +492,14 @@ class ZimManageViewModel @Inject constructor(
val books = args[ZERO] as List<BookOnDisk> val books = args[ZERO] as List<BookOnDisk>
val activeDownloads = args[ONE] as List<DownloadModel> val activeDownloads = args[ONE] as List<DownloadModel>
val languageList = args[TWO] as List<Language> val languageList = args[TWO] as List<Language>
val libraryNetworkEntity = args[THREE] as List<org.kiwix.libkiwix.Book> val libraryNetworkEntity = args[THREE] as List<LibkiwixBook>
val filter = args[FOUR] as String val filter = args[FOUR] as String
val fileSystemState = args[FIVE] as FileSystemState val fileSystemState = args[FIVE] as FileSystemState
combineLibrarySources( combineLibrarySources(
booksOnFileSystem = books, booksOnFileSystem = books,
activeDownloads = activeDownloads, activeDownloads = activeDownloads,
allLanguages = languageList, allLanguages = languageList,
libraryNetworkEntity = libraryNetworkEntity, onlineBooks = libraryNetworkEntity,
filter = filter, filter = filter,
fileSystemState = fileSystemState fileSystemState = fileSystemState
) )
@ -509,7 +514,7 @@ class ZimManageViewModel @Inject constructor(
} }
private fun updateLanguagesInDao( private fun updateLanguagesInDao(
library: MutableSharedFlow<List<org.kiwix.libkiwix.Book>>, library: MutableSharedFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>, languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = ) =
@ -527,7 +532,7 @@ class ZimManageViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
private fun combineToLanguageList( private fun combineToLanguageList(
booksFromNetwork: List<org.kiwix.libkiwix.Book>, booksFromNetwork: List<LibkiwixBook>,
allLanguages: List<Language> allLanguages: List<Language>
) = when { ) = when {
booksFromNetwork.isEmpty() -> { booksFromNetwork.isEmpty() -> {
@ -551,8 +556,8 @@ class ZimManageViewModel @Inject constructor(
) )
} }
private fun networkLanguageCounts(booksFromNetwork: List<org.kiwix.libkiwix.Book>) = private fun networkLanguageCounts(booksFromNetwork: List<LibkiwixBook>) =
booksFromNetwork.mapNotNull { it.language } booksFromNetwork.map { it.language }
.fold( .fold(
mutableMapOf<String, Int>() mutableMapOf<String, Int>()
) { acc, language -> acc.increment(language) } ) { acc, language -> acc.increment(language) }
@ -589,7 +594,7 @@ class ZimManageViewModel @Inject constructor(
booksOnFileSystem: List<BookOnDisk>, booksOnFileSystem: List<BookOnDisk>,
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
allLanguages: List<Language>, allLanguages: List<Language>,
onlineBooks: List<org.kiwix.libkiwix.Book>, onlineBooks: List<LibkiwixBook>,
filter: String, filter: String,
fileSystemState: FileSystemState fileSystemState: FileSystemState
): List<LibraryListItem> { ): List<LibraryListItem> {
@ -634,7 +639,7 @@ class ZimManageViewModel @Inject constructor(
} }
private fun createLibrarySection( private fun createLibrarySection(
books: List<Book>, books: List<LibkiwixBook>,
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
fileSystemState: FileSystemState, fileSystemState: FileSystemState,
sectionStringId: Int, sectionStringId: Int,
@ -648,7 +653,7 @@ class ZimManageViewModel @Inject constructor(
} }
private fun applySearchFilter( private fun applySearchFilter(
unDownloadedBooks: List<Book>, unDownloadedBooks: List<LibkiwixBook>,
filter: String filter: String
) = if (filter.isEmpty()) { ) = if (filter.isEmpty()) {
unDownloadedBooks unDownloadedBooks
@ -657,7 +662,7 @@ class ZimManageViewModel @Inject constructor(
unDownloadedBooks.filter { it.searchMatches > 0 } unDownloadedBooks.filter { it.searchMatches > 0 }
} }
private fun List<Book>.asLibraryItems( private fun List<LibkiwixBook>.asLibraryItems(
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
fileSystemState: FileSystemState fileSystemState: FileSystemState
) = map { book -> ) = map { book ->

View File

@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.settings.StorageCalculator import org.kiwix.kiwixmobile.core.settings.StorageCalculator
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import javax.inject.Inject import javax.inject.Inject
@ -49,6 +49,6 @@ class AvailableSpaceCalculator @Inject constructor(
} }
} }
suspend fun hasAvailableSpaceForBook(book: Book) = suspend fun hasAvailableSpaceForBook(book: LibkiwixBook) =
book.size.toLong() * KB < storageCalculator.availableBytes() book.size.toLong() * KB < storageCalculator.availableBytes()
} }

View File

@ -24,14 +24,14 @@ import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
import org.kiwix.kiwixmobile.core.downloader.model.Seconds import org.kiwix.kiwixmobile.core.downloader.model.Seconds
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag
import org.kiwix.kiwixmobile.zimManager.Fat32Checker import org.kiwix.kiwixmobile.zimManager.Fat32Checker
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.DetectingFileSystem import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.DetectingFileSystem
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
sealed class LibraryListItem { sealed class LibraryListItem {
abstract val id: Long abstract val id: Long
@ -42,7 +42,7 @@ sealed class LibraryListItem {
) : LibraryListItem() ) : LibraryListItem()
data class BookItem constructor( data class BookItem constructor(
val book: Book, val book: LibkiwixBook,
val fileSystemState: FileSystemState, val fileSystemState: FileSystemState,
val tags: List<KiwixTag> = KiwixTag.from(book.tags), val tags: List<KiwixTag> = KiwixTag.from(book.tags),
override val id: Long = book.id.hashCode().toLong() override val id: Long = book.id.hashCode().toLong()
@ -54,7 +54,7 @@ sealed class LibraryListItem {
} }
companion object { companion object {
private fun Book.isLessThan4GB() = private fun LibkiwixBook.isLessThan4GB() =
size.toLongOrNull() ?: 0L < Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES size.toLongOrNull() ?: 0L < Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES
} }
} }

View File

@ -63,6 +63,12 @@ object Libs {
const val tracing: String = "androidx.tracing:tracing:" + Versions.tracing const val tracing: String = "androidx.tracing:tracing:" + Versions.tracing
/**
* https://github.com/square/retrofit
*/
const val converter_scalars: String = "com.squareup.retrofit2:converter-scalars:" +
Versions.com_squareup_retrofit2
/** /**
* https://github.com/square/retrofit * https://github.com/square/retrofit
*/ */

View File

@ -47,6 +47,11 @@ dependencies {
implementation(Libs.select_folder_document_file) implementation(Libs.select_folder_document_file)
// Square // Square
implementation(Libs.converter_scalars) {
exclude(group = "xpp3", module = "xpp3")
exclude(group = "stax", module = "stax-api")
exclude(group = "stax", module = "stax")
}
implementation(Libs.converter_simplexml) { implementation(Libs.converter_simplexml) {
exclude(group = "xpp3", module = "xpp3") exclude(group = "xpp3", module = "xpp3")
exclude(group = "stax", module = "stax-api") exclude(group = "stax", module = "stax-api")

View File

@ -50,6 +50,7 @@
<ID>PackageNaming:TagsView.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID> <ID>PackageNaming:TagsView.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:ConnectivityBroadcastReceiver.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID> <ID>PackageNaming:ConnectivityBroadcastReceiver.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:NetworkState.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID> <ID>PackageNaming:NetworkState.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:OnlineLibraryManager.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun getAllZimParts(book: Book): List&lt;File></ID> <ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun getAllZimParts(book: Book): List&lt;File></ID>
<ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun getLocalFilePathByUri( context: Context, uri: Uri ): String?</ID> <ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun getLocalFilePathByUri( context: Context, uri: Uri ): String?</ID>
<ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun hasPart(file: File): Boolean</ID> <ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun hasPart(file: File): Boolean</ID>

View File

@ -33,7 +33,7 @@ import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.downloader.DownloadRequester import org.kiwix.kiwixmobile.core.downloader.DownloadRequester
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
import javax.inject.Inject import javax.inject.Inject
@ -100,7 +100,7 @@ abstract class DownloadRoomDao {
fun addIfDoesNotExist( fun addIfDoesNotExist(
url: String, url: String,
book: LibraryNetworkEntity.Book, book: LibkiwixBook,
downloadRequester: DownloadRequester downloadRequester: DownloadRequester
) { ) {
if (doesNotAlreadyExist(book)) { if (doesNotAlreadyExist(book)) {
@ -113,6 +113,6 @@ abstract class DownloadRoomDao {
} }
} }
private fun doesNotAlreadyExist(book: LibraryNetworkEntity.Book) = private fun doesNotAlreadyExist(book: LibkiwixBook) =
count(book.id) == 0 count(book.id) == 0
} }

View File

@ -20,7 +20,6 @@ package org.kiwix.kiwixmobile.core.dao
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.util.Base64
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -36,11 +35,11 @@ import org.kiwix.kiwixmobile.core.DarkModeConfig
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
import org.kiwix.kiwixmobile.core.extensions.deleteFile import org.kiwix.kiwixmobile.core.extensions.deleteFile
import org.kiwix.kiwixmobile.core.extensions.getFavicon
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE
import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
@ -285,10 +284,7 @@ class LibkiwixBookmarks @Inject constructor(
} }
// Check if the book has an illustration of the specified size and encode it to Base64. // Check if the book has an illustration of the specified size and encode it to Base64.
val favicon = val favicon = book?.getFavicon()
book?.getIllustration(ILLUSTRATION_SIZE)?.data?.let {
Base64.encodeToString(it, Base64.DEFAULT)
}
val zimReaderSource = book?.path?.let { ZimReaderSource(File(it)) } val zimReaderSource = book?.path?.let { ZimReaderSource(File(it)) }
// Return the LibkiwixBookmarkItem, filtering out null results. // Return the LibkiwixBookmarkItem, filtering out null results.

View File

@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity_ import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity_
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import javax.inject.Inject import javax.inject.Inject
@ -124,7 +124,7 @@ class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) {
} }
@Suppress("UnsafeCallOnNullableType") @Suppress("UnsafeCallOnNullableType")
fun migrationInsert(books: List<Book>) { fun migrationInsert(books: List<LibkiwixBook>) {
insert(books.map { BookOnDisk(book = it, zimReaderSource = ZimReaderSource(it.file!!)) }) insert(books.map { BookOnDisk(book = it, zimReaderSource = ZimReaderSource(it.file!!)) })
} }

View File

@ -22,7 +22,7 @@ import io.objectbox.annotation.Convert
import io.objectbox.annotation.Entity import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id import io.objectbox.annotation.Id
import io.objectbox.converter.PropertyConverter import io.objectbox.converter.PropertyConverter
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
@ -70,7 +70,7 @@ data class BookOnDiskEntity(
bookOnDisk.book.tags bookOnDisk.book.tags
) )
fun toBook() = Book().apply { fun toBook() = LibkiwixBook().apply {
id = bookId id = bookId
title = this@BookOnDiskEntity.title title = this@BookOnDiskEntity.title
description = this@BookOnDiskEntity.description description = this@BookOnDiskEntity.description

View File

@ -20,12 +20,12 @@ package org.kiwix.kiwixmobile.core.dao.entities
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import io.objectbox.annotation.Convert
import io.objectbox.converter.PropertyConverter
import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Error import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import io.objectbox.annotation.Convert
import io.objectbox.converter.PropertyConverter
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
@Entity @Entity
data class DownloadRoomEntity( data class DownloadRoomEntity(
@ -56,7 +56,7 @@ data class DownloadRoomEntity(
val favIcon: String, val favIcon: String,
val tags: String? = null val tags: String? = null
) { ) {
constructor(downloadId: Long, book: Book) : this( constructor(downloadId: Long, book: LibkiwixBook) : this(
downloadId = downloadId, downloadId = downloadId,
bookId = book.id, bookId = book.id,
title = book.title, title = book.title,
@ -75,7 +75,7 @@ data class DownloadRoomEntity(
) )
fun toBook() = fun toBook() =
Book().apply { LibkiwixBook().apply {
id = bookId id = bookId
title = this@DownloadRoomEntity.title title = this@DownloadRoomEntity.title
description = this@DownloadRoomEntity.description description = this@DownloadRoomEntity.description

View File

@ -71,7 +71,7 @@ class Repository @Inject internal constructor(
// Split languages if there are multiple, otherwise return the single book. Bug fix #3892 // Split languages if there are multiple, otherwise return the single book. Bug fix #3892
if (bookOnDisk.book.language.contains(',')) { if (bookOnDisk.book.language.contains(',')) {
bookOnDisk.book.language.split(',').map { lang -> bookOnDisk.book.language.split(',').map { lang ->
bookOnDisk.copy(book = bookOnDisk.book.copy(language = lang.trim())) bookOnDisk.copy(book = bookOnDisk.book.copy(_language = lang.trim()))
} }
} else { } else {
listOf(bookOnDisk) listOf(bookOnDisk)

View File

@ -22,6 +22,7 @@ package org.kiwix.kiwixmobile.core.data.remote
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Url import retrofit2.http.Url
@ -41,6 +42,8 @@ interface KiwixService {
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(ScalarsConverterFactory.create())
// .addConverterFactory(SimpleXmlConverterFactory.create())
.build() .build()
return retrofit.create(KiwixService::class.java) return retrofit.create(KiwixService::class.java)
} }
@ -49,6 +52,6 @@ interface KiwixService {
companion object { companion object {
// To fetch the full OPDS catalog. // To fetch the full OPDS catalog.
// TODO we will change this to pagination later once we migrate to OPDS properly. // TODO we will change this to pagination later once we migrate to OPDS properly.
const val OPDS_LIBRARY_NETWORK_PATH = "/entries?count=-1" const val OPDS_LIBRARY_NETWORK_PATH = "entries?count=-1"
} }
} }

View File

@ -58,6 +58,7 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchResultGenerator import org.kiwix.kiwixmobile.core.search.viewmodel.SearchResultGenerator
import org.kiwix.kiwixmobile.core.utils.BookUtils import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@ -99,6 +100,7 @@ interface CoreComponent {
fun connectivityManager(): ConnectivityManager fun connectivityManager(): ConnectivityManager
fun objectBoxToLibkiwixMigrator(): ObjectBoxToLibkiwixMigrator fun objectBoxToLibkiwixMigrator(): ObjectBoxToLibkiwixMigrator
fun libkiwixBookmarks(): LibkiwixBookmarks fun libkiwixBookmarks(): LibkiwixBookmarks
fun onlineLibraryManager(): OnlineLibraryManager
fun recentSearchRoomDao(): RecentSearchRoomDao fun recentSearchRoomDao(): RecentSearchRoomDao
fun historyRoomDao(): HistoryRoomDao fun historyRoomDao(): HistoryRoomDao
fun webViewHistoryRoomDao(): WebViewHistoryRoomDao fun webViewHistoryRoomDao(): WebViewHistoryRoomDao

View File

@ -28,7 +28,6 @@ import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager
import org.kiwix.libkiwix.JNIKiwix import org.kiwix.libkiwix.JNIKiwix
import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager import org.kiwix.libkiwix.Manager
import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -38,39 +37,27 @@ class JNIModule {
@Provides @Provides
@Singleton @Singleton
@Named("bookmarks")
fun provideLibrary(): Library = Library() fun provideLibrary(): Library = Library()
@Provides @Provides
@Singleton @Singleton
@Named("bookmarks")
fun providesManager(library: Library): Manager = Manager(library) fun providesManager(library: Library): Manager = Manager(library)
@Provides @Provides
@Singleton @Singleton
fun providesLibkiwixBookmarks( fun providesLibkiwixBookmarks(
@Named("bookmarks") library: Library, library: Library,
@Named("bookmarks") manager: Manager, manager: Manager,
sharedPreferenceUtil: SharedPreferenceUtil, sharedPreferenceUtil: SharedPreferenceUtil,
bookDao: NewBookDao, bookDao: NewBookDao,
zimReaderContainer: ZimReaderContainer zimReaderContainer: ZimReaderContainer
): LibkiwixBookmarks = ): LibkiwixBookmarks =
LibkiwixBookmarks(library, manager, sharedPreferenceUtil, bookDao, zimReaderContainer) LibkiwixBookmarks(library, manager, sharedPreferenceUtil, bookDao, zimReaderContainer)
@Provides
@Singleton
@Named("onlineLibrary")
fun provideOnlineLibrary(): Library = Library()
@Provides
@Singleton
@Named("onlineLibrary")
fun providesOnlineManager(library: Library): Manager = Manager(library)
@Provides @Provides
@Singleton @Singleton
fun provideOnlineLibraryParser( fun provideOnlineLibraryParser(
@Named("onlineLibrary") library: Library, library: Library,
@Named("onlineLibrary") manager: Manager manager: Manager
) = OnlineLibraryManager(library, manager) ): OnlineLibraryManager = OnlineLibraryManager(library, manager)
} }

View File

@ -38,7 +38,7 @@ const val CONNECTION_TIMEOUT = 10L
const val READ_TIMEOUT = 300L const val READ_TIMEOUT = 300L
const val CALL_TIMEOUT = 300L const val CALL_TIMEOUT = 300L
const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}" const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}"
const val KIWIX_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/v2" const val KIWIX_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/v2/"
@Module @Module
class NetworkModule { class NetworkModule {

View File

@ -17,10 +17,10 @@
*/ */
package org.kiwix.kiwixmobile.core.downloader package org.kiwix.kiwixmobile.core.downloader
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
interface Downloader { interface Downloader {
fun download(book: LibraryNetworkEntity.Book) fun download(book: LibkiwixBook)
fun cancelDownload(downloadId: Long) fun cancelDownload(downloadId: Long)
fun retryDownload(downloadId: Long) fun retryDownload(downloadId: Long)
fun pauseResumeDownload(downloadId: Long, isPause: Boolean) fun pauseResumeDownload(downloadId: Long, isPause: Boolean)

View File

@ -23,8 +23,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.data.remote.KiwixService import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import javax.inject.Inject import javax.inject.Inject
class DownloaderImpl @Inject constructor( class DownloaderImpl @Inject constructor(
@ -33,7 +32,7 @@ class DownloaderImpl @Inject constructor(
private val kiwixService: KiwixService private val kiwixService: KiwixService
) : Downloader { ) : Downloader {
@Suppress("InjectDispatcher") @Suppress("InjectDispatcher")
override fun download(book: LibraryNetworkEntity.Book) { override fun download(book: LibkiwixBook) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
runCatching { runCatching {
urlProvider(book)?.let { urlProvider(book)?.let {
@ -46,7 +45,7 @@ class DownloaderImpl @Inject constructor(
} }
@Suppress("UnsafeCallOnNullableType") @Suppress("UnsafeCallOnNullableType")
private suspend fun urlProvider(book: Book): String? = private suspend fun urlProvider(book: LibkiwixBook): String? =
if (book.url?.endsWith("meta4") == true) { if (book.url?.endsWith("meta4") == true) {
kiwixService.getMetaLinks(book.url!!)?.relevantUrl?.value kiwixService.getMetaLinks(book.url!!)?.relevantUrl?.value
} else { } else {

View File

@ -17,10 +17,10 @@
*/ */
package org.kiwix.kiwixmobile.core.downloader.model package org.kiwix.kiwixmobile.core.downloader.model
import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2.Error import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Status
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.utils.StorageUtils import org.kiwix.kiwixmobile.core.utils.StorageUtils
data class DownloadModel( data class DownloadModel(
@ -33,7 +33,7 @@ data class DownloadModel(
val state: Status, val state: Status,
val error: Error, val error: Error,
val progress: Int, val progress: Int,
val book: Book val book: LibkiwixBook
) { ) {
val bytesRemaining: Long by lazy { totalSizeOfDownload - bytesDownloaded } val bytesRemaining: Long by lazy { totalSizeOfDownload - bytesDownloaded }
val fileNameFromUrl: String by lazy { StorageUtils.getFileNameFromUrl(book.url) } val fileNameFromUrl: String by lazy { StorageUtils.getFileNameFromUrl(book.url) }

View File

@ -0,0 +1,145 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.entity
import org.kiwix.kiwixmobile.core.extensions.getFavicon
import org.kiwix.libkiwix.Book
import java.io.File
/**
* Wrapper around libkiwix's [Book] that allows setting custom values (e.g. from DB or UI),
* while still falling back to the original [nativeBook]'s properties when not provided.
*/
@Suppress("ConstructorParameterNaming")
data class LibkiwixBook(
private val nativeBook: Book? = null,
private var _id: String = "",
private var _title: String = "",
private var _description: String? = null,
private var _language: String = "",
private var _creator: String = "",
private var _publisher: String = "",
private var _date: String = "",
private var _url: String? = null,
private var _articleCount: String? = null,
private var _mediaCount: String? = null,
private var _size: String = "",
private var _bookName: String? = null,
private var _favicon: String = "",
private var _tags: String? = null,
var searchMatches: Int = 0,
var file: File? = null
) {
var id: String
get() = _id.ifEmpty { nativeBook?.id.orEmpty() }
set(id) {
_id = id
}
var title: String
get() = _title.ifEmpty { nativeBook?.title.orEmpty() }
set(title) {
_title = title
}
var description: String?
get() = _description ?: nativeBook?.description
set(description) {
_description = description
}
var language: String
get() = _language.ifEmpty { nativeBook?.language.orEmpty() }
set(language) {
_language = language
}
var creator: String
get() = _creator.ifEmpty { nativeBook?.creator.orEmpty() }
set(creator) {
_creator = creator
}
var publisher: String
get() = _publisher.ifEmpty { nativeBook?.publisher.orEmpty() }
set(publisher) {
_publisher = publisher
}
var date: String
get() = _date.ifEmpty { nativeBook?.date.orEmpty() }
set(date) {
_date = date
}
var url: String?
get() = _url ?: nativeBook?.url
set(url) {
_url = url
}
var articleCount: String?
get() = _articleCount ?: nativeBook?.articleCount?.toString()
set(articleCount) {
_articleCount = articleCount
}
var mediaCount: String?
get() = _mediaCount ?: nativeBook?.mediaCount?.toString()
set(mediaCount) {
_mediaCount = mediaCount
}
var size: String
get() = _size.ifEmpty { nativeBook?.size?.toString().orEmpty() }
set(size) {
_size = size
}
var bookName: String?
get() = _bookName ?: nativeBook?.name
set(bookName) {
_bookName = bookName
}
var favicon: String
get() = _favicon.ifEmpty { nativeBook?.getFavicon().orEmpty() }
set(favicon) {
_favicon = favicon
}
var tags: String?
get() = _tags ?: nativeBook?.tags
set(tags) {
_tags = tags
}
// Two books are equal if their ids match
override fun equals(other: Any?): Boolean {
if (other is LibkiwixBook) {
if (other.id == id) {
return true
}
}
return false
}
// Only use the book's id to generate a hash code
override fun hashCode(): Int = id.hashCode()
}

View File

@ -18,30 +18,32 @@
package org.kiwix.kiwixmobile.core.extensions package org.kiwix.kiwixmobile.core.extensions
import android.util.Base64
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE
import org.kiwix.kiwixmobile.core.utils.BookUtils import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.NetworkUtils import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.libkiwix.Book
fun Book.calculateSearchMatches( fun LibkiwixBook.calculateSearchMatches(
filter: String, filter: String,
bookUtils: BookUtils bookUtils: BookUtils
) { ) {
val searchableText = buildSearchableText(bookUtils) val searchableText = buildSearchableText(bookUtils)
searchMatches = filter.split("\\s+") searchMatches = filter.split("\\s+")
.foldRight( .foldRight(
0, 0
{ filterWord, acc -> ) { filterWord, acc ->
if (searchableText.contains(filterWord, true)) { if (searchableText.contains(filterWord, true)) {
acc + 1 acc + 1
} else { } else {
acc acc
}
} }
) }
} }
fun Book.buildSearchableText(bookUtils: BookUtils): String = fun LibkiwixBook.buildSearchableText(bookUtils: BookUtils): String =
StringBuilder().apply { StringBuilder().apply {
append(title) append(title)
append("|") append("|")
@ -54,3 +56,7 @@ fun Book.buildSearchableText(bookUtils: BookUtils): String =
append("|") append("|")
} }
}.toString() }.toString()
fun Book.getFavicon(): String? = getIllustration(ILLUSTRATION_SIZE)?.data?.let {
Base64.encodeToString(it, Base64.DEFAULT)
}

View File

@ -28,7 +28,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.DarkModeConfig import org.kiwix.kiwixmobile.core.DarkModeConfig
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS
import org.kiwix.kiwixmobile.core.main.UNINITIALISE_HTML import org.kiwix.kiwixmobile.core.main.UNINITIALISE_HTML
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX
@ -377,7 +377,7 @@ class ZimFileReader constructor(
@Suppress("ExplicitThis") // this@ZimFileReader.name is required @Suppress("ExplicitThis") // this@ZimFileReader.name is required
fun toBook() = fun toBook() =
Book().apply { LibkiwixBook().apply {
title = this@ZimFileReader.title title = this@ZimFileReader.title
id = this@ZimFileReader.id id = this@ZimFileReader.id
size = "$fileSize" size = "$fileSize"

View File

@ -18,11 +18,12 @@
package org.kiwix.kiwixmobile.core.zim_manager package org.kiwix.kiwixmobile.core.zim_manager
import org.kiwix.libkiwix.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager import org.kiwix.libkiwix.Manager
import javax.inject.Inject
class OnlineLibraryManager( class OnlineLibraryManager @Inject constructor(
val library: Library, val library: Library,
val manager: Manager val manager: Manager
) { ) {
@ -33,12 +34,12 @@ class OnlineLibraryManager(
it.printStackTrace() it.printStackTrace()
}.isSuccess }.isSuccess
suspend fun getOnlineBooks(): List<Book> { suspend fun getOnlineBooks(): List<LibkiwixBook> {
val onlineBooksList = arrayListOf<Book>() val onlineBooksList = arrayListOf<LibkiwixBook>()
runCatching { runCatching {
library.booksIds.forEach { bookId -> library.booksIds.forEach { bookId ->
val book = library.getBookById(bookId) val book = library.getBookById(bookId)
onlineBooksList.add(book) onlineBooksList.add(LibkiwixBook(book))
} }
}.onFailure { it.printStackTrace() } }.onFailure { it.printStackTrace() }
return onlineBooksList return onlineBooksList

View File

@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.convertToLocal import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.convertToLocal
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag
@ -44,7 +44,7 @@ sealed class BooksOnDiskListItem {
data class BookOnDisk constructor( data class BookOnDisk constructor(
val databaseId: Long = 0L, val databaseId: Long = 0L,
val book: LibraryNetworkEntity.Book, val book: LibkiwixBook,
val file: File = File(""), val file: File = File(""),
val zimReaderSource: ZimReaderSource, val zimReaderSource: ZimReaderSource,
val tags: List<KiwixTag> = KiwixTag.Companion.from(book.tags), val tags: List<KiwixTag> = KiwixTag.Companion.from(book.tags),

View File

@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.custom.download.effects
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.downloader.Downloader import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.custom.BuildConfig import org.kiwix.kiwixmobile.custom.BuildConfig
import javax.inject.Inject import javax.inject.Inject
@ -45,7 +45,7 @@ data class DownloadCustom @Inject constructor(val downloader: Downloader) : Side
name: String = "", name: String = "",
favIcon: String = "" favIcon: String = ""
) = ) =
Book().apply { LibkiwixBook().apply {
this.id = id this.id = id
this.title = title this.title = title
this.description = description this.description = description