Merge pull request #4333 from kiwix/Fixes#4298

Introduced support of OPDS catalog.
This commit is contained in:
Kelson 2025-06-07 13:29:33 +02:00 committed by GitHub
commit 18e3b3d9b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 504 additions and 349 deletions

View File

@ -55,13 +55,6 @@
<init>(...); <init>(...);
} }
## keep everything in LibraryNetworkEntity.kt
-keep class org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity { *; }
-keep class org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity$* { *; }
-keepclassmembers class org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity$* {
<init>(...);
}
-keep class javax.xml.stream.** { *; } -keep class javax.xml.stream.** { *; }
-dontwarn javax.xml.stream.Location -dontwarn javax.xml.stream.Location
-dontwarn javax.xml.stream.XMLEventReader -dontwarn javax.xml.stream.XMLEventReader

View File

@ -127,7 +127,7 @@ class DownloadRobot : BaseRobot() {
testFlakyView({ testFlakyView({
composeTestRule.apply { composeTestRule.apply {
waitUntilTimeout() waitUntilTimeout()
onAllNodesWithTag(ONLINE_BOOK_ITEM_TESTING_TAG)[0].performClick() onAllNodesWithTag(ONLINE_BOOK_ITEM_TESTING_TAG)[1].performClick()
} }
}) })
} }

View File

@ -48,7 +48,7 @@ import org.kiwix.kiwixmobile.core.di.modules.CALL_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.ui.components.SWIPE_REFRESH_TESTING_TAG import org.kiwix.kiwixmobile.core.ui.components.SWIPE_REFRESH_TESTING_TAG
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import java.io.File import java.io.File
@ -150,7 +150,7 @@ object TestUtils {
@JvmStatic fun withContent(content: String): Matcher<Any?> { @JvmStatic fun withContent(content: String): Matcher<Any?> {
return object : BoundedMatcher<Any?, Any?>(Any::class.java) { return object : BoundedMatcher<Any?, Any?>(Any::class.java) {
public override fun matchesSafely(myObj: Any?): Boolean { public override fun matchesSafely(myObj: Any?): Boolean {
if (myObj !is LibraryNetworkEntity.Book) { if (myObj !is LibkiwixBook) {
return false return false
} }
return if (myObj.url != null) { return if (myObj.url != null) {

View File

@ -35,7 +35,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER
import org.kiwix.kiwixmobile.core.utils.files.DocumentResolverWrapper import org.kiwix.kiwixmobile.core.utils.files.DocumentResolverWrapper
import org.kiwix.kiwixmobile.core.utils.files.FileUtils import org.kiwix.kiwixmobile.core.utils.files.FileUtils
@ -109,7 +109,7 @@ class FileUtilsInstrumentationTest {
} }
char1++ char1++
} }
val book = LibraryNetworkEntity.Book() val book = LibkiwixBook()
book.file = File(fileName + "bg") book.file = File(fileName + "bg")
val files = getAllZimParts(book) val files = getAllZimParts(book)

View File

@ -44,7 +44,6 @@ import androidx.compose.ui.semantics.testTag
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.downloader.model.toPainter
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle
import org.kiwix.kiwixmobile.core.ui.models.IconItem import org.kiwix.kiwixmobile.core.ui.models.IconItem
@ -104,7 +103,7 @@ private fun DownloadBookContent(
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
BookIcon(item.favIcon.toPainter()) BookIcon(item.favIconUrl, isOnlineLibrary = true)
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)

View File

@ -48,8 +48,6 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.toPainter
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.PureGrey import org.kiwix.kiwixmobile.core.ui.theme.PureGrey
@ -58,7 +56,7 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONLINE_BOOK_DISABLED_COLOR_ALPHA import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONLINE_BOOK_DISABLED_COLOR_ALPHA
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
import org.kiwix.kiwixmobile.core.zim_manager.KiloByte import org.kiwix.kiwixmobile.core.zim_manager.Byte
import org.kiwix.kiwixmobile.ui.BookDate import org.kiwix.kiwixmobile.ui.BookDate
import org.kiwix.kiwixmobile.ui.BookDescription import org.kiwix.kiwixmobile.ui.BookDescription
import org.kiwix.kiwixmobile.ui.BookIcon import org.kiwix.kiwixmobile.ui.BookIcon
@ -167,7 +165,7 @@ private fun OnlineBookContent(item: BookItem, bookUtils: BookUtils) {
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
BookIcon(Base64String(item.book.favicon).toPainter()) BookIcon(item.book.favicon, isOnlineLibrary = true)
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
@ -207,7 +205,7 @@ private fun BookSizeAndDateRow(item: BookItem) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
BookSize( BookSize(
KiloByte(item.book.size).humanReadable, Byte(item.book.size).humanReadable,
modifier = Modifier.weight(1f).testTag(ONLINE_BOOK_SIZE_TEXT_TESTING_TAG) modifier = Modifier.weight(1f).testTag(ONLINE_BOOK_SIZE_TEXT_TESTING_TAG)
) )
BookDate(item.book.date) BookDate(item.book.date)

View File

@ -19,6 +19,7 @@
package org.kiwix.kiwixmobile.ui package org.kiwix.kiwixmobile.ui
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -31,18 +32,17 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import coil3.compose.AsyncImage
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.toPainter import org.kiwix.kiwixmobile.core.downloader.model.toPainter
@ -53,7 +53,7 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
import org.kiwix.kiwixmobile.core.zim_manager.KiloByte import org.kiwix.kiwixmobile.core.zim_manager.Byte
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.ArticleCount import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.ArticleCount
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode
@ -116,7 +116,7 @@ private fun BookContent(
if (selectionMode == SelectionMode.MULTI) { if (selectionMode == SelectionMode.MULTI) {
BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick, index) BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick, index)
} }
BookIcon(Base64String(bookOnDisk.book.favicon).toPainter()) BookIcon(bookOnDisk.book.favicon, isOnlineLibrary = false)
BookDetails(Modifier.weight(1f), bookOnDisk) BookDetails(Modifier.weight(1f), bookOnDisk)
} }
} }
@ -142,14 +142,23 @@ private fun BookCheckbox(
} }
@Composable @Composable
fun BookIcon(painter: Painter) { fun BookIcon(iconSource: String, isOnlineLibrary: Boolean) {
Icon( val modifier = Modifier.size(BOOK_ICON_SIZE)
painter = painter, if (isOnlineLibrary) {
contentDescription = stringResource(R.string.fav_icon), AsyncImage(
modifier = Modifier model = iconSource,
.size(BOOK_ICON_SIZE), contentDescription = stringResource(R.string.fav_icon),
tint = Color.Unspecified modifier = modifier,
) placeholder = painterResource(R.drawable.default_zim_file_icon),
error = painterResource(R.drawable.default_zim_file_icon),
)
} else {
Image(
painter = Base64String(iconSource).toPainter(),
contentDescription = stringResource(R.string.fav_icon),
modifier = modifier
)
}
} }
@Composable @Composable
@ -164,7 +173,7 @@ private fun BookDetails(modifier: Modifier, bookOnDisk: BookOnDisk) {
) { ) {
BookDate(bookOnDisk.book.date) BookDate(bookOnDisk.book.date)
Spacer(modifier = Modifier.width(EIGHT_DP)) Spacer(modifier = Modifier.width(EIGHT_DP))
BookSize(KiloByte(bookOnDisk.book.size).humanReadable) BookSize(Byte(bookOnDisk.book.size).humanReadable)
Spacer(modifier = Modifier.width(EIGHT_DP)) Spacer(modifier = Modifier.width(EIGHT_DP))
BookArticleCount( BookArticleCount(
ArticleCount(bookOnDisk.book.articleCount.orEmpty()) ArticleCount(bookOnDisk.book.articleCount.orEmpty())

View File

@ -22,18 +22,18 @@ import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.data.remote.OnlineLibraryProgressListener import org.kiwix.kiwixmobile.core.data.remote.OnlineLibraryProgressListener
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DEFAULT_INT_VALUE import org.kiwix.kiwixmobile.core.downloader.downloadManager.DEFAULT_INT_VALUE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED
import org.kiwix.kiwixmobile.core.downloader.downloadManager.NINE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
class AppProgressListenerProvider( class AppProgressListenerProvider(
private val zimManageViewModel: ZimManageViewModel private val zimManageViewModel: ZimManageViewModel
) : OnlineLibraryProgressListener { ) : OnlineLibraryProgressListener {
@Suppress("MagicNumber")
override fun onProgress(bytesRead: Long, contentLength: Long) { override fun onProgress(bytesRead: Long, contentLength: Long) {
val progress = val progress =
if (contentLength == DEFAULT_INT_VALUE.toLong()) { if (contentLength == DEFAULT_INT_VALUE.toLong()) {
ZERO ZERO
} else { } else {
(bytesRead * 3 * HUNDERED / contentLength).coerceAtMost(HUNDERED.toLong()) (bytesRead * NINE * HUNDERED / contentLength).coerceAtMost(HUNDERED.toLong())
} }
zimManageViewModel.downloadProgress.postValue( zimManageViewModel.downloadProgress.postValue(
zimManageViewModel.context.getString( zimManageViewModel.context.getString(

View File

@ -70,20 +70,19 @@ import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.data.DataSource import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.LIBRARY_NETWORK_PATH import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.OPDS_LIBRARY_NETWORK_PATH
import org.kiwix.kiwixmobile.core.data.remote.ProgressResponseBody import org.kiwix.kiwixmobile.core.data.remote.ProgressResponseBody
import org.kiwix.kiwixmobile.core.data.remote.UserAgentInterceptor import org.kiwix.kiwixmobile.core.data.remote.UserAgentInterceptor
import org.kiwix.kiwixmobile.core.di.modules.CALL_TIMEOUT import org.kiwix.kiwixmobile.core.di.modules.CALL_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.KIWIX_DOWNLOAD_URL import org.kiwix.kiwixmobile.core.di.modules.KIWIX_OPDS_LIBRARY_URL
import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DEFAULT_INT_VALUE 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.entity.LibraryNetworkEntity.Book
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
@ -97,6 +96,7 @@ import org.kiwix.kiwixmobile.core.zim_manager.ConnectivityBroadcastReceiver
import org.kiwix.kiwixmobile.core.zim_manager.Language import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.NetworkState import org.kiwix.kiwixmobile.core.zim_manager.NetworkState
import org.kiwix.kiwixmobile.core.zim_manager.NetworkState.CONNECTED import org.kiwix.kiwixmobile.core.zim_manager.NetworkState.CONNECTED
import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI
@ -121,7 +121,8 @@ import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.BookItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.BookItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem
import java.util.LinkedList import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject import javax.inject.Inject
@ -132,6 +133,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,
@ -145,7 +147,7 @@ class ZimManageViewModel @Inject constructor(
private val defaultLanguageProvider: DefaultLanguageProvider, private val defaultLanguageProvider: DefaultLanguageProvider,
private val dataSource: DataSource, private val dataSource: DataSource,
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
private val sharedPreferenceUtil: SharedPreferenceUtil private val sharedPreferenceUtil: SharedPreferenceUtil,
) : ViewModel() { ) : ViewModel() {
sealed class FileSelectActions { sealed class FileSelectActions {
data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions() data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions()
@ -158,6 +160,12 @@ class ZimManageViewModel @Inject constructor(
object UserClickedDownloadBooksButton : FileSelectActions() object UserClickedDownloadBooksButton : FileSelectActions()
} }
private val library by lazy { Library() }
private val manager by lazy { Manager(library) }
private val onlineLibraryManager by lazy {
OnlineLibraryManager(library, manager)
}
private var isUnitTestCase: Boolean = false private var isUnitTestCase: Boolean = false
val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow() val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow()
private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList()) private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList())
@ -168,7 +176,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<LibraryNetworkEntity>(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>(
@ -238,7 +246,10 @@ class ZimManageViewModel @Inject constructor(
} ?: originalResponse } ?: originalResponse
} }
.build() .build()
return KiwixService.ServiceCreator.newHackListService(customOkHttpClient, KIWIX_DOWNLOAD_URL) return KiwixService.ServiceCreator.newHackListService(
customOkHttpClient,
KIWIX_OPDS_LIBRARY_URL
)
.also { .also {
kiwixService = it kiwixService = it
} }
@ -249,7 +260,7 @@ class ZimManageViewModel @Inject constructor(
private fun getContentLengthOfLibraryXmlFile(): Long { private fun getContentLengthOfLibraryXmlFile(): Long {
val headRequest = val headRequest =
Request.Builder() Request.Builder()
.url("$KIWIX_DOWNLOAD_URL$LIBRARY_NETWORK_PATH") .url("$KIWIX_OPDS_LIBRARY_URL$OPDS_LIBRARY_NETWORK_PATH")
.head() .head()
.header("Accept-Encoding", "identity") .header("Accept-Encoding", "identity")
.build() .build()
@ -388,7 +399,7 @@ class ZimManageViewModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun requestsAndConnectivityChangesToLibraryRequests( private fun requestsAndConnectivityChangesToLibraryRequests(
library: MutableSharedFlow<LibraryNetworkEntity>, library: MutableSharedFlow<List<LibkiwixBook>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = requestDownloadLibrary.flatMapConcat { ) = requestDownloadLibrary.flatMapConcat {
connectivityBroadcastReceiver.networkStates connectivityBroadcastReceiver.networkStates
@ -408,6 +419,7 @@ class ZimManageViewModel @Inject constructor(
it.printStackTrace().also { it.printStackTrace().also {
isOnlineLibraryDownloading = false isOnlineLibraryDownloading = false
onlineLibraryDownloading.tryEmit(false) onlineLibraryDownloading.tryEmit(false)
library.emit(emptyList())
} }
} }
.onEach { .onEach {
@ -439,16 +451,27 @@ class ZimManageViewModel @Inject constructor(
private fun downloadLibraryFlow( private fun downloadLibraryFlow(
kiwixService: KiwixService kiwixService: KiwixService
): Flow<LibraryNetworkEntity?> = 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()
val resolvedUrl = response.raw().networkResponse?.request?.url
?: response.raw().request.url
val baseHostUrl = "${resolvedUrl.scheme}://${resolvedUrl.host}"
downloadProgress.postValue(context.getString(R.string.parsing_remote_library)) downloadProgress.postValue(context.getString(R.string.parsing_remote_library))
emit(response) val libraryXml = response.body()
val isLibraryParsed = onlineLibraryManager.parseOPDSStream(libraryXml, baseHostUrl)
emit(
if (isLibraryParsed) {
onlineLibraryManager.getOnlineBooks()
} else {
emptyList()
}
)
} }
.retry(5) .retry(5)
.catch { e -> .catch { e ->
e.printStackTrace() e.printStackTrace()
emit(LibraryNetworkEntity().apply { book = LinkedList() }) emit(emptyList())
} }
private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates
@ -460,7 +483,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<LibraryNetworkEntity>, 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) {
@ -483,14 +506,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 LibraryNetworkEntity 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
) )
@ -505,12 +528,12 @@ class ZimManageViewModel @Inject constructor(
} }
private fun updateLanguagesInDao( private fun updateLanguagesInDao(
library: MutableSharedFlow<LibraryNetworkEntity>, library: MutableSharedFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>, languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = ) =
combine( combine(
library.map { it.book }.filterNotNull(), library,
languages languages
) { books, existingLanguages -> ) { books, existingLanguages ->
combineToLanguageList(books, existingLanguages) combineToLanguageList(books, existingLanguages)
@ -523,7 +546,7 @@ class ZimManageViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
private fun combineToLanguageList( private fun combineToLanguageList(
booksFromNetwork: List<Book>, booksFromNetwork: List<LibkiwixBook>,
allLanguages: List<Language> allLanguages: List<Language>
) = when { ) = when {
booksFromNetwork.isEmpty() -> { booksFromNetwork.isEmpty() -> {
@ -547,8 +570,8 @@ class ZimManageViewModel @Inject constructor(
) )
} }
private fun networkLanguageCounts(booksFromNetwork: List<Book>) = private fun networkLanguageCounts(booksFromNetwork: List<LibkiwixBook>) =
booksFromNetwork.mapNotNull(Book::language) booksFromNetwork.map { it.language }
.fold( .fold(
mutableMapOf<String, Int>() mutableMapOf<String, Int>()
) { acc, language -> acc.increment(language) } ) { acc, language -> acc.increment(language) }
@ -585,14 +608,14 @@ class ZimManageViewModel @Inject constructor(
booksOnFileSystem: List<BookOnDisk>, booksOnFileSystem: List<BookOnDisk>,
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
allLanguages: List<Language>, allLanguages: List<Language>,
libraryNetworkEntity: LibraryNetworkEntity, onlineBooks: List<LibkiwixBook>,
filter: String, filter: String,
fileSystemState: FileSystemState fileSystemState: FileSystemState
): List<LibraryListItem> { ): List<LibraryListItem> {
val activeLanguageCodes = val activeLanguageCodes =
allLanguages.filter(Language::active) allLanguages.filter(Language::active)
.map(Language::languageCode) .map(Language::languageCode)
val allBooks = libraryNetworkEntity.book!! - booksOnFileSystem.map(BookOnDisk::book).toSet() val allBooks = onlineBooks - booksOnFileSystem.map(BookOnDisk::book).toSet()
val downloadingBooks = val downloadingBooks =
activeDownloads.mapNotNull { download -> activeDownloads.mapNotNull { download ->
allBooks.firstOrNull { it.id == download.book.id } allBooks.firstOrNull { it.id == download.book.id }
@ -630,7 +653,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,
@ -644,7 +667,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
@ -653,7 +676,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

@ -19,12 +19,11 @@
package org.kiwix.kiwixmobile.zimManager.libraryView package org.kiwix.kiwixmobile.zimManager.libraryView
import eu.mhutti1.utils.storage.Bytes import eu.mhutti1.utils.storage.Bytes
import eu.mhutti1.utils.storage.KB
import kotlinx.coroutines.flow.first 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
@ -42,13 +41,13 @@ class AvailableSpaceCalculator @Inject constructor(
.map { downloads -> downloads.sumOf(DownloadModel::bytesRemaining) } .map { downloads -> downloads.sumOf(DownloadModel::bytesRemaining) }
.map { bytesToBeDownloaded -> storageCalculator.availableBytes() - bytesToBeDownloaded } .map { bytesToBeDownloaded -> storageCalculator.availableBytes() - bytesToBeDownloaded }
.first() .first()
if (bookItem.book.size.toLong() * KB < trueAvailableBytes) { if (bookItem.book.size.toLong() < trueAvailableBytes) {
successAction.invoke(bookItem) successAction.invoke(bookItem)
} else { } else {
failureAction.invoke(Bytes(trueAvailableBytes).humanReadable) failureAction.invoke(Bytes(trueAvailableBytes).humanReadable)
} }
} }
suspend fun hasAvailableSpaceForBook(book: Book) = suspend fun hasAvailableSpaceForBook(book: LibkiwixBook) =
book.size.toLong() * KB < storageCalculator.availableBytes() book.size.toLong() < storageCalculator.availableBytes()
} }

View File

@ -20,18 +20,17 @@ package org.kiwix.kiwixmobile.zimManager.libraryView.adapter
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
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 +41,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,14 +53,14 @@ 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
} }
} }
data class LibraryDownloadItem( data class LibraryDownloadItem(
val downloadId: Long, val downloadId: Long,
val favIcon: Base64String, val favIconUrl: String,
val title: String, val title: String,
val description: String?, val description: String?,
val bytesDownloaded: Long, val bytesDownloaded: Long,
@ -76,7 +75,7 @@ sealed class LibraryListItem {
constructor(downloadModel: DownloadModel) : this( constructor(downloadModel: DownloadModel) : this(
downloadModel.downloadId, downloadModel.downloadId,
Base64String(downloadModel.book.favicon), downloadModel.book.favicon,
downloadModel.book.title, downloadModel.book.title,
downloadModel.book.description, downloadModel.book.description,
downloadModel.bytesDownloaded, downloadModel.bytesDownloaded,

View File

@ -28,7 +28,6 @@ import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test import app.cash.turbine.test
import com.jraska.livedata.test import com.jraska.livedata.test
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -60,7 +59,7 @@ import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.data.DataSource import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService import org.kiwix.kiwixmobile.core.data.remote.KiwixService
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.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.utils.dialog.AlertDialogShower import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
@ -90,11 +89,10 @@ import org.kiwix.kiwixmobile.zimManager.fileselectView.effects.ShareFiles
import org.kiwix.kiwixmobile.zimManager.fileselectView.effects.StartMultiSelection import org.kiwix.kiwixmobile.zimManager.fileselectView.effects.StartMultiSelection
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import org.kiwix.sharedFunctions.InstantExecutorExtension import org.kiwix.sharedFunctions.InstantExecutorExtension
import org.kiwix.sharedFunctions.book
import org.kiwix.sharedFunctions.bookOnDisk import org.kiwix.sharedFunctions.bookOnDisk
import org.kiwix.sharedFunctions.downloadModel import org.kiwix.sharedFunctions.downloadModel
import org.kiwix.sharedFunctions.language import org.kiwix.sharedFunctions.language
import org.kiwix.sharedFunctions.libraryNetworkEntity import org.kiwix.sharedFunctions.libkiwixBook
import java.util.Locale import java.util.Locale
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -191,7 +189,7 @@ class ZimManageViewModelTest {
setAlertDialogShower(alertDialogShower) setAlertDialogShower(alertDialogShower)
} }
viewModel.fileSelectListStates.value = FileSelectListState(emptyList()) viewModel.fileSelectListStates.value = FileSelectListState(emptyList())
runBlocking { viewModel.networkLibrary.emit(libraryNetworkEntity()) } runBlocking { viewModel.networkLibrary.emit(emptyList()) }
} }
@Nested @Nested
@ -238,8 +236,8 @@ class ZimManageViewModelTest {
@Test @Test
fun `books found on filesystem are filtered by books already in db`() = runTest { fun `books found on filesystem are filtered by books already in db`() = runTest {
every { application.getString(any()) } returns "" every { application.getString(any()) } returns ""
val expectedBook = bookOnDisk(1L, book("1")) val expectedBook = bookOnDisk(1L, libkiwixBook("1"))
val bookToRemove = bookOnDisk(1L, book("2")) val bookToRemove = bookOnDisk(1L, libkiwixBook("2"))
advanceUntilIdle() advanceUntilIdle()
viewModel.requestFileSystemCheck.emit(Unit) viewModel.requestFileSystemCheck.emit(Unit)
advanceUntilIdle() advanceUntilIdle()
@ -314,9 +312,9 @@ class ZimManageViewModelTest {
) )
expectNetworkDbAndDefault( expectNetworkDbAndDefault(
listOf( listOf(
book(language = "eng"), libkiwixBook(language = "eng"),
book(language = "eng"), libkiwixBook(language = "eng"),
book(language = "fra") libkiwixBook(language = "fra")
), ),
listOf(), listOf(),
defaultLanguage defaultLanguage
@ -352,9 +350,9 @@ class ZimManageViewModelTest {
) )
expectNetworkDbAndDefault( expectNetworkDbAndDefault(
listOf( listOf(
book(language = "eng"), libkiwixBook(language = "eng"),
book(language = "eng"), libkiwixBook(language = "eng"),
book(language = "fra") libkiwixBook(language = "fra")
), ),
listOf(dbLanguage), listOf(dbLanguage),
language(isActive = true, occurencesOfLanguage = 1) language(isActive = true, occurencesOfLanguage = 1)
@ -378,15 +376,14 @@ class ZimManageViewModelTest {
} }
private suspend fun TestScope.expectNetworkDbAndDefault( private suspend fun TestScope.expectNetworkDbAndDefault(
networkBooks: List<Book>, networkBooks: List<LibkiwixBook>,
dbBooks: List<Language>, dbBooks: List<Language>,
defaultLanguage: Language defaultLanguage: Language
) { ) {
every { application.getString(any()) } returns "" every { application.getString(any()) } returns ""
every { application.getString(any(), any()) } returns "" every { application.getString(any(), any()) } returns ""
coEvery { kiwixService.getLibrary() } returns libraryNetworkEntity(networkBooks)
every { defaultLanguageProvider.provide() } returns defaultLanguage every { defaultLanguageProvider.provide() } returns defaultLanguage
viewModel.networkLibrary.emit(libraryNetworkEntity(networkBooks)) viewModel.networkLibrary.emit(networkBooks)
runCurrent() runCurrent()
languages.value = dbBooks languages.value = dbBooks
runCurrent() runCurrent()
@ -405,10 +402,10 @@ class ZimManageViewModelTest {
@Test @Test
fun `library update removes from sources and maps to list items`() = runTest { fun `library update removes from sources and maps to list items`() = runTest {
val bookAlreadyOnDisk = book(id = "0", url = "", language = Locale.ENGLISH.language) val bookAlreadyOnDisk = libkiwixBook(id = "0", url = "", language = Locale.ENGLISH.language)
val bookDownloading = book(id = "1", url = "") val bookDownloading = libkiwixBook(id = "1", url = "")
val bookWithActiveLanguage = book(id = "3", language = "activeLanguage", url = "") val bookWithActiveLanguage = libkiwixBook(id = "3", language = "activeLanguage", url = "")
val bookWithInactiveLanguage = book(id = "4", language = "inactiveLanguage", url = "") val bookWithInactiveLanguage = libkiwixBook(id = "4", language = "inactiveLanguage", url = "")
testFlow( testFlow(
flow = viewModel.libraryItems, flow = viewModel.libraryItems,
triggerAction = { triggerAction = {
@ -429,13 +426,11 @@ class ZimManageViewModelTest {
fileSystemStates.tryEmit(CanWrite4GbFile) fileSystemStates.tryEmit(CanWrite4GbFile)
advanceUntilIdle() advanceUntilIdle()
viewModel.networkLibrary.emit( viewModel.networkLibrary.emit(
libraryNetworkEntity( listOf(
listOf( bookAlreadyOnDisk,
bookAlreadyOnDisk, bookDownloading,
bookDownloading, bookWithActiveLanguage,
bookWithActiveLanguage, bookWithInactiveLanguage
bookWithInactiveLanguage
)
) )
) )
}, },
@ -458,7 +453,7 @@ class ZimManageViewModelTest {
@Test @Test
fun `library marks files over 4GB as can't download if file system state says to`() = runTest { fun `library marks files over 4GB as can't download if file system state says to`() = runTest {
val bookOver4Gb = val bookOver4Gb =
book( libkiwixBook(
id = "0", id = "0",
url = "", url = "",
size = "${Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES + 1}" size = "${Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES + 1}"
@ -477,7 +472,7 @@ class ZimManageViewModelTest {
) )
) )
fileSystemStates.tryEmit(CannotWrite4GbFile) fileSystemStates.tryEmit(CannotWrite4GbFile)
viewModel.networkLibrary.emit(libraryNetworkEntity(listOf(bookOver4Gb))) viewModel.networkLibrary.emit(listOf(bookOver4Gb))
}, },
assert = { assert = {
skipItems(1) skipItems(1)

View File

@ -24,17 +24,17 @@ import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
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
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.BookItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.BookItem
internal class LibraryListItemTest { internal class LibraryListItemTest {
private val book = mockk<Book>() private val book = mockk<LibkiwixBook>()
@BeforeEach @BeforeEach
fun init() { fun init() {
@ -76,6 +76,6 @@ internal class LibraryListItemTest {
assertThat(canBeDownloaded(book, NotEnoughSpaceFor4GbFile)).isTrue assertThat(canBeDownloaded(book, NotEnoughSpaceFor4GbFile)).isTrue
} }
private fun canBeDownloaded(book: Book, fileSystemState: FileSystemState) = private fun canBeDownloaded(book: LibkiwixBook, fileSystemState: FileSystemState) =
BookItem(book, fileSystemState).canBeDownloaded BookItem(book, fileSystemState).canBeDownloaded
} }

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
*/ */
@ -363,4 +369,7 @@ object Libs {
const val COMPOSE_LIVE_DATA = const val COMPOSE_LIVE_DATA =
"androidx.compose.runtime:runtime-livedata:${Versions.COMPOSE_VERSION}" "androidx.compose.runtime:runtime-livedata:${Versions.COMPOSE_VERSION}"
const val COIL3_COMPOSE = "io.coil-kt.coil3:coil-compose:${Versions.COIL_COMPOSE}"
const val COIL3_OKHTTP_COMPOSE = "io.coil-kt.coil3:coil-network-okhttp:${Versions.COIL_COMPOSE}"
} }

View File

@ -115,6 +115,8 @@ object Versions {
const val COMPOSE_MATERIAL3 = "1.3.1" const val COMPOSE_MATERIAL3 = "1.3.1"
const val TURBINE_FLOW_TEST = "1.2.0" const val TURBINE_FLOW_TEST = "1.2.0"
const val COIL_COMPOSE = "3.2.0"
} }
/** /**

View File

@ -243,6 +243,8 @@ class AllProjectConfigurer {
implementation(Libs.ANDROIDX_ACTIVITY_COMPOSE) implementation(Libs.ANDROIDX_ACTIVITY_COMPOSE)
implementation(Libs.COMPOSE_TOOLING_PREVIEW) implementation(Libs.COMPOSE_TOOLING_PREVIEW)
implementation(Libs.COMPOSE_LIVE_DATA) implementation(Libs.COMPOSE_LIVE_DATA)
implementation(Libs.COIL3_COMPOSE)
implementation(Libs.COIL3_OKHTTP_COMPOSE)
// Compose UI test implementation // Compose UI test implementation
androidTestImplementation(Libs.COMPOSE_UI_TEST_JUNIT) androidTestImplementation(Libs.COMPOSE_UI_TEST_JUNIT)

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

@ -21,7 +21,7 @@
<ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID> <ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID>
<ID>MagicNumber:FileUtils.kt$FileUtils$3</ID> <ID>MagicNumber:FileUtils.kt$FileUtils$3</ID>
<ID>MagicNumber:JNIInitialiser.kt$JNIInitialiser$1024</ID> <ID>MagicNumber:JNIInitialiser.kt$JNIInitialiser$1024</ID>
<ID>MagicNumber:KiloByte.kt$KiloByte$1024.0</ID> <ID>MagicNumber:Byte.kt$Byte$1024.0</ID>
<ID>MagicNumber:MainMenu.kt$MainMenu$99</ID> <ID>MagicNumber:MainMenu.kt$MainMenu$99</ID>
<ID>MagicNumber:OnSwipeTouchListener.kt$OnSwipeTouchListener.GestureListener$100</ID> <ID>MagicNumber:OnSwipeTouchListener.kt$OnSwipeTouchListener.GestureListener$100</ID>
<ID>MagicNumber:SearchResultGenerator.kt$ZimSearchResultGenerator$200</ID> <ID>MagicNumber:SearchResultGenerator.kt$ZimSearchResultGenerator$200</ID>
@ -42,7 +42,7 @@
<ID>NestedBlockDepth:StorageDeviceUtils.kt$StorageDeviceUtils$// Amazingly file.canWrite() does not always return the correct value private fun canWrite(file: File): Boolean</ID> <ID>NestedBlockDepth:StorageDeviceUtils.kt$StorageDeviceUtils$// Amazingly file.canWrite() does not always return the correct value private fun canWrite(file: File): Boolean</ID>
<ID>PackageNaming:ArticleCount.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID> <ID>PackageNaming:ArticleCount.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID>
<ID>PackageNaming:BooksOnDiskListItem.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID> <ID>PackageNaming:BooksOnDiskListItem.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID>
<ID>PackageNaming:KiloByte.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID> <ID>PackageNaming:Byte.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:KiwixTag.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID> <ID>PackageNaming:KiwixTag.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:Language.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID> <ID>PackageNaming:Language.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:MountPointProducer.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID> <ID>PackageNaming:MountPointProducer.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
@ -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

@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.core.dao package org.kiwix.kiwixmobile.core.dao
import android.util.Base64
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
@ -25,16 +26,20 @@ import androidx.room.Query
import androidx.room.Update import androidx.room.Update
import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Status.COMPLETED import com.tonyodev.fetch2.Status.COMPLETED
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity 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.reader.ILLUSTRATION_SIZE
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
import org.kiwix.libzim.Archive
import javax.inject.Inject import javax.inject.Inject
@Dao @Dao
@ -53,15 +58,37 @@ abstract class DownloadRoomDao {
fun allDownloads() = getAllDownloads().map { it.map(::DownloadModel) } fun allDownloads() = getAllDownloads().map { it.map(::DownloadModel) }
private fun moveCompletedToBooksOnDiskDao(downloadEntities: List<DownloadRoomEntity>) { @Suppress("InjectDispatcher")
private suspend fun moveCompletedToBooksOnDiskDao(downloadEntities: List<DownloadRoomEntity>) {
downloadEntities.filter { it.status == COMPLETED } downloadEntities.filter { it.status == COMPLETED }
.takeIf(List<DownloadRoomEntity>::isNotEmpty) .takeIf(List<DownloadRoomEntity>::isNotEmpty)
?.let { ?.let { completedDownloads ->
deleteDownloadsList(it) deleteDownloadsList(completedDownloads)
newBookDao.insert(it.map(BooksOnDiskListItem::BookOnDisk)) // We now use the OPDS stream instead of the custom library.xml handling.
// In the OPDS stream, the favicon is a URL instead of a Base64 string.
// So when a download is completed, we extract the illustration directly from the archive.
val booksOnDisk = completedDownloads.map { download ->
val archive = withContext(Dispatchers.IO) {
Archive(download.file)
}
val favicon = getOnlineBookFaviconForOfflineUsages(archive).orEmpty()
val updatedEntity = download.copy(favIcon = favicon)
BooksOnDiskListItem.BookOnDisk(updatedEntity)
}
newBookDao.insert(booksOnDisk)
} }
} }
private fun getOnlineBookFaviconForOfflineUsages(archive: Archive): String? =
if (archive.hasIllustration(ILLUSTRATION_SIZE)) {
Base64.encodeToString(
archive.getIllustrationItem(ILLUSTRATION_SIZE).data.data,
Base64.DEFAULT
)
} else {
null
}
fun update(download: Download) { fun update(download: Download) {
getEntityForDownloadId(download.id.toLong())?.let { downloadRoomEntity -> getEntityForDownloadId(download.id.toLong())?.let { downloadRoomEntity ->
downloadRoomEntity.updateWith(download) downloadRoomEntity.updateWith(download)
@ -100,7 +127,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 +140,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
@ -205,7 +204,7 @@ class LibkiwixBookmarks @Inject constructor(
library.addBook(libKiwixBook).also { library.addBook(libKiwixBook).also {
// now library has changed so update our library list. // now library has changed so update our library list.
libraryBooksList = library.booksIds.toList() libraryBooksList = library.booksIds.toList()
Log.d( Log.e(
TAG, TAG,
"Added Book to Library:\n" + "Added Book to Library:\n" +
"ZIM File Path: ${book.path}\n" + "ZIM File Path: ${book.path}\n" +
@ -234,6 +233,7 @@ class LibkiwixBookmarks @Inject constructor(
CoroutineScope(dispatcher).launch { CoroutineScope(dispatcher).launch {
writeBookMarksAndSaveLibraryToFile() writeBookMarksAndSaveLibraryToFile()
updateFlowBookmarkList() updateFlowBookmarkList()
removeBookFromLibraryIfNoRelatedBookmarksAreStored(dispatcher, bookmarks)
} }
} }
} }
@ -242,6 +242,34 @@ class LibkiwixBookmarks @Inject constructor(
deleteBookmarks(listOf(LibkiwixBookmarkItem(zimId = bookId, bookmarkUrl = bookmarkUrl))) deleteBookmarks(listOf(LibkiwixBookmarkItem(zimId = bookId, bookmarkUrl = bookmarkUrl)))
} }
/**
* Removes books from the library that no longer have any associated bookmarks.
*
* This function checks if any of the books associated with the given deleted bookmarks
* are still referenced by other existing bookmarks. If not, those books are removed from the library.
*
* @param dispatcher The coroutine dispatcher to run the operation on (typically Dispatchers.IO).
* @param deletedBookmarks The list of bookmarks that were just deleted.
*/
private suspend fun removeBookFromLibraryIfNoRelatedBookmarksAreStored(
dispatcher: CoroutineDispatcher,
deletedBookmarks: List<LibkiwixBookmarkItem>
) {
withContext(dispatcher) {
val currentBookmarks = getBookmarksList()
val deletedZimIds = deletedBookmarks.map { it.zimId }.distinct()
deletedZimIds.forEach { zimId ->
val stillExists = currentBookmarks.any { it.zimId == zimId }
if (!stillExists) {
library.removeBookById(zimId)
Log.d(TAG, "Removed book from library since no bookmarks exist for: $zimId")
}
}
}
writeBookMarksAndSaveLibraryToFile()
}
/** /**
* Asynchronously writes the library and bookmarks data to their respective files in a background thread * Asynchronously writes the library and bookmarks data to their respective files in a background thread
* to prevent potential data loss and ensures that the library holds the updated ZIM file paths and favicons. * to prevent potential data loss and ensures that the library holds the updated ZIM file paths and favicons.
@ -285,10 +313,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

@ -20,16 +20,17 @@
package org.kiwix.kiwixmobile.core.data.remote package org.kiwix.kiwixmobile.core.data.remote
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity
import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.converter.simplexml.SimpleXmlConverterFactory import retrofit2.converter.simplexml.SimpleXmlConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Url import retrofit2.http.Url
interface KiwixService { interface KiwixService {
@GET(LIBRARY_NETWORK_PATH) @GET(OPDS_LIBRARY_NETWORK_PATH)
suspend fun getLibrary(): LibraryNetworkEntity? suspend fun getLibrary(): Response<String>
@GET @GET
suspend fun getMetaLinks( suspend fun getMetaLinks(
@ -43,6 +44,7 @@ interface KiwixService {
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl(baseUrl) .baseUrl(baseUrl)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(SimpleXmlConverterFactory.create()) .addConverterFactory(SimpleXmlConverterFactory.create())
.build() .build()
return retrofit.create(KiwixService::class.java) return retrofit.create(KiwixService::class.java)
@ -50,6 +52,8 @@ interface KiwixService {
} }
companion object { companion object {
const val LIBRARY_NETWORK_PATH = "/library/library_zim.xml" // To fetch the full OPDS catalog.
// TODO we will change this to pagination later once we migrate to OPDS properly.
const val OPDS_LIBRARY_NETWORK_PATH = "v2/entries?count=-1"
} }
} }

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_DOWNLOAD_URL = "https://mirror.download.kiwix.org/" const val KIWIX_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/"
@Module @Module
class NetworkModule { class NetworkModule {
@ -59,5 +59,5 @@ class NetworkModule {
} }
@Provides @Singleton fun provideKiwixService(okHttpClient: OkHttpClient): KiwixService = @Provides @Singleton fun provideKiwixService(okHttpClient: OkHttpClient): KiwixService =
ServiceCreator.newHackListService(okHttpClient, KIWIX_DOWNLOAD_URL) ServiceCreator.newHackListService(okHttpClient, KIWIX_OPDS_LIBRARY_URL)
} }

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

@ -37,6 +37,7 @@ import javax.inject.Inject
const val ZERO = 0 const val ZERO = 0
const val FIVE = 5 const val FIVE = 5
const val SIX = 6 const val SIX = 6
const val NINE = 9
const val HUNDERED = 100 const val HUNDERED = 100
const val DEFAULT_INT_VALUE = -1 const val DEFAULT_INT_VALUE = -1

View File

@ -38,7 +38,7 @@ value class Base64String(private val encodedString: String?) {
BitmapFactory.decodeByteArray(it, 0, it.size) BitmapFactory.decodeByteArray(it, 0, it.size)
} }
} }
} catch (illegalArgumentException: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
null null
} }
} }

View File

@ -34,7 +34,7 @@ import org.kiwix.kiwixmobile.core.R
data class DownloadItem( data class DownloadItem(
val downloadId: Long, val downloadId: Long,
val favIcon: Base64String, val favIconUrl: String,
val title: String, val title: String,
val description: String?, val description: String?,
val bytesDownloaded: Long, val bytesDownloaded: Long,
@ -47,7 +47,7 @@ data class DownloadItem(
constructor(downloadModel: DownloadModel) : this( constructor(downloadModel: DownloadModel) : this(
downloadModel.downloadId, downloadModel.downloadId,
Base64String(downloadModel.book.favicon), downloadModel.book.favicon,
downloadModel.book.title, downloadModel.book.title,
downloadModel.book.description, downloadModel.book.description,
downloadModel.bytesDownloaded, downloadModel.bytesDownloaded,

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

@ -1,134 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.simpleframework.xml.Attribute
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root
import java.io.File
import java.io.Serializable
import java.util.LinkedList
@Root(name = "library")
class LibraryNetworkEntity {
@field:ElementList(name = "book", inline = true, required = false)
var book: LinkedList<Book>? = null
@field:Attribute(name = "version", required = false)
var version: String? = null
@Root(name = "book", strict = false)
class Book : Serializable {
@field:Attribute(name = "id", required = false)
var id: String = ""
@field:Attribute(name = "title", required = false)
var title: String = ""
@field:Attribute(name = "description", required = false)
var description: String? = null
@field:Attribute(name = "language", required = false)
var language: String = ""
@field:Attribute(name = "creator", required = false)
var creator: String = ""
@field:Attribute(name = "publisher", required = false)
var publisher: String = ""
@field:Attribute(name = "favicon", required = false)
var favicon: String = ""
@field:Attribute(name = "faviconMimeType", required = false)
var faviconMimeType: String? = null
@field:Attribute(name = "date", required = false)
var date: String = ""
@field:Attribute(name = "url", required = false)
var url: String? = null
@field:Attribute(name = "articleCount", required = false)
var articleCount: String? = null
@field:Attribute(name = "mediaCount", required = false)
var mediaCount: String? = null
@field:Attribute(name = "size", required = false)
var size: String = ""
@field:Attribute(name = "name", required = false)
var bookName: String? = null
@field:Attribute(name = "tags", required = false)
var tags: String? = null
var searchMatches = 0
var file: File? = null
// Two books are equal if their ids match
override fun equals(other: Any?): Boolean {
if (other is Book) {
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()
@Suppress("LongParameterList")
fun copy(
language: String = this.language,
title: String = this.title,
description: String? = this.description,
creator: String = this.creator,
publisher: String = this.publisher,
favicon: String = this.favicon,
faviconMimeType: String? = this.faviconMimeType,
date: String = this.date,
url: String? = this.url,
articleCount: String? = this.articleCount,
mediaCount: String? = this.mediaCount,
size: String = this.size,
bookName: String? = this.bookName,
tags: String? = this.tags
): Book {
return Book().apply {
this.id = this@Book.id
this.title = title
this.description = description
this.language = language
this.creator = creator
this.publisher = publisher
this.favicon = favicon
this.faviconMimeType = faviconMimeType
this.date = date
this.url = url
this.articleCount = articleCount
this.mediaCount = mediaCount
this.size = size
this.bookName = bookName
this.tags = tags
}
}
}
}

View File

@ -18,30 +18,33 @@
package org.kiwix.kiwixmobile.core.extensions package org.kiwix.kiwixmobile.core.extensions
import android.util.Base64
import android.util.Log
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 +57,21 @@ fun Book.buildSearchableText(bookUtils: BookUtils): String =
append("|") append("|")
} }
}.toString() }.toString()
fun Book?.getFavicon(): String? =
runCatching {
val illustration = this?.getIllustration(ILLUSTRATION_SIZE)
illustration?.url()?.ifBlank {
illustration.data?.let {
Base64.encodeToString(it, Base64.DEFAULT)
}
}
}.getOrElse {
it.printStackTrace().also {
this?.illustrations?.forEach { illustration ->
Log.e("BOOK", "getFavicon: ${illustration.data} and ${illustration.url()}")
}
Log.e("BOOK", "getFavicon: ${this?.title}")
}
""
}

View File

@ -24,20 +24,6 @@ import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
fun ImageView.setBitmap(base64String: Base64String) {
base64String.toBitmap()
?.let(::setImageBitmap)
?: kotlin.run { setImageDrawableCompat(R.drawable.default_zim_file_icon) }
}
// methods that accept inline classes as parameters are not allowed to be called from java
// hence this facade
fun ImageView.setBitmapFromString(string: String?) {
setBitmap(Base64String(string))
}
fun ImageView.setImageDrawableCompat( fun ImageView.setImageDrawableCompat(
@DrawableRes id: Int @DrawableRes id: Int

View File

@ -340,6 +340,7 @@ abstract class CoreReaderFragment :
private val navigationHistoryList: MutableList<NavigationHistoryListItem> = ArrayList() private val navigationHistoryList: MutableList<NavigationHistoryListItem> = ArrayList()
private var isReadSelection = false private var isReadSelection = false
private var isReadAloudServiceRunning = false private var isReadAloudServiceRunning = false
private var libkiwixBook: Book? = null
private var readerLifeCycleScope: CoroutineScope? = null private var readerLifeCycleScope: CoroutineScope? = null
val coreReaderLifeCycleScope: CoroutineScope? val coreReaderLifeCycleScope: CoroutineScope?
@ -2038,9 +2039,7 @@ abstract class CoreReaderFragment :
lifecycleScope.launch { lifecycleScope.launch {
getCurrentWebView()?.url?.let { articleUrl -> getCurrentWebView()?.url?.let { articleUrl ->
zimReaderContainer?.zimFileReader?.let { zimFileReader -> zimReaderContainer?.zimFileReader?.let { zimFileReader ->
val libKiwixBook = Book().apply { val libKiwixBook = getLibkiwixBook(zimFileReader)
update(zimFileReader.jniKiwixReader)
}
if (isBookmarked) { if (isBookmarked) {
repositoryActions?.deleteBookmark(libKiwixBook.id, articleUrl) repositoryActions?.deleteBookmark(libKiwixBook.id, articleUrl)
snackBarRoot?.snack(R.string.bookmark_removed) snackBarRoot?.snack(R.string.bookmark_removed)
@ -2071,6 +2070,19 @@ abstract class CoreReaderFragment :
} }
} }
/**
* Returns the libkiwix book evertime when user saves or remove the bookmark.
* the object will be created once to avoid creating it multiple times.
*/
private fun getLibkiwixBook(zimFileReader: ZimFileReader): Book {
libkiwixBook?.let { return it }
val book = Book().apply {
update(zimFileReader.jniKiwixReader)
}
libkiwixBook = book
return book
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateBottomToolbarVisibility() updateBottomToolbarVisibility()

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

@ -42,7 +42,7 @@ import kotlinx.coroutines.sync.withLock
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.ChunkUtils import org.kiwix.kiwixmobile.core.downloader.ChunkUtils
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.extensions.deleteFile import org.kiwix.kiwixmobile.core.extensions.deleteFile
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
@ -494,7 +494,7 @@ object FileUtils {
@Suppress("NestedBlockDepth") @Suppress("NestedBlockDepth")
@JvmStatic @JvmStatic
suspend fun getAllZimParts(book: Book): List<File> { suspend fun getAllZimParts(book: LibkiwixBook): List<File> {
val files = ArrayList<File>() val files = ArrayList<File>()
book.file?.let { book.file?.let {
if (it.path.endsWith(".zim") || it.path.endsWith(".zim.part")) { if (it.path.endsWith(".zim") || it.path.endsWith(".zim.part")) {

View File

@ -23,10 +23,10 @@ import kotlin.math.log10
import kotlin.math.pow import kotlin.math.pow
@JvmInline @JvmInline
value class KiloByte(private val kilobyteString: String?) { value class Byte(private val byteString: String?) {
val humanReadable val humanReadable
get() = kilobyteString?.toLongOrNull()?.let { get() = byteString?.toLongOrNull()?.let {
val units = arrayOf("KB", "MB", "GB", "TB") val units = arrayOf("B", "KB", "MB", "GB", "TB")
val conversion = (log10(it.toDouble()) / log10(1024.0)).toInt() val conversion = (log10(it.toDouble()) / log10(1024.0)).toInt()
DecimalFormat("#,##0.#") DecimalFormat("#,##0.#")
.format(it / 1024.0.pow(conversion.toDouble())) + .format(it / 1024.0.pow(conversion.toDouble())) +

View File

@ -0,0 +1,43 @@
/*
* 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.zim_manager
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager
class OnlineLibraryManager(val library: Library, val manager: Manager) {
suspend fun parseOPDSStream(content: String?, urlHost: String): Boolean =
runCatching {
manager.readOpds(content, urlHost)
}.onFailure {
it.printStackTrace()
}.isSuccess
suspend fun getOnlineBooks(): List<LibkiwixBook> {
val onlineBooksList = arrayListOf<LibkiwixBook>()
runCatching {
library.booksIds.forEach { bookId ->
val book = library.getBookById(bookId)
onlineBooksList.add(LibkiwixBook(book))
}
}.onFailure { it.printStackTrace() }
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

@ -22,14 +22,12 @@ import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2.Status.NONE import com.tonyodev.fetch2.Status.NONE
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem
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.DownloadState.Pending import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Pending
import org.kiwix.kiwixmobile.core.downloader.model.Seconds import org.kiwix.kiwixmobile.core.downloader.model.Seconds
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.FileElement import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.FileElement
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.Pieces import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.Pieces
@ -38,11 +36,10 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.zim_manager.Language import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import java.io.File import java.io.File
import java.util.LinkedList
fun bookOnDisk( fun bookOnDisk(
databaseId: Long = 0L, databaseId: Long = 0L,
book: Book = book(), book: LibkiwixBook = libkiwixBook(),
zimReaderSource: ZimReaderSource = ZimReaderSource(File("")) zimReaderSource: ZimReaderSource = ZimReaderSource(File(""))
) = BookOnDisk(databaseId, book, File(""), zimReaderSource) ) = BookOnDisk(databaseId, book, File(""), zimReaderSource)
@ -56,7 +53,7 @@ fun downloadModel(
status: Status = NONE, status: Status = NONE,
error: Error = Error.NONE, error: Error = Error.NONE,
progress: Int = 1, progress: Int = 1,
book: Book = book() book: LibkiwixBook = libkiwixBook()
) = DownloadModel( ) = DownloadModel(
databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload, databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload,
status, error, progress, book status, error, progress, book
@ -64,7 +61,7 @@ fun downloadModel(
fun downloadItem( fun downloadItem(
downloadId: Long = 1L, downloadId: Long = 1L,
favIcon: Base64String = Base64String("favIcon"), favIcon: String = "favIcon",
title: String = "title", title: String = "title",
description: String = "description", description: String = "description",
bytesDownloaded: Long = 1L, bytesDownloaded: Long = 1L,
@ -135,7 +132,7 @@ fun url(
this.value = value this.value = value
} }
fun book( fun libkiwixBook(
id: String = "id", id: String = "id",
title: String = "title", title: String = "title",
description: String = "description", description: String = "description",
@ -150,7 +147,7 @@ fun book(
name: String = "name", name: String = "name",
favIcon: String = "favIcon", favIcon: String = "favIcon",
file: File = File("") file: File = File("")
) = Book().apply { ) = LibkiwixBook().apply {
this.id = id this.id = id
this.title = title this.title = title
this.description = description this.description = description
@ -167,11 +164,6 @@ fun book(
favicon = favIcon favicon = favIcon
} }
fun libraryNetworkEntity(books: List<Book> = emptyList()) =
LibraryNetworkEntity().apply {
book = LinkedList(books)
}
fun recentSearchEntity( fun recentSearchEntity(
id: Long = 0L, id: Long = 0L,
searchTerm: String = "", searchTerm: String = "",

View File

@ -40,7 +40,7 @@ import org.kiwix.kiwixmobile.core.utils.files.FileSearch
import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener
import org.kiwix.kiwixmobile.core.utils.files.testFlow import org.kiwix.kiwixmobile.core.utils.files.testFlow
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.sharedFunctions.book import org.kiwix.sharedFunctions.libkiwixBook
import org.kiwix.sharedFunctions.bookOnDisk import org.kiwix.sharedFunctions.bookOnDisk
import java.io.File import java.io.File
@ -85,7 +85,7 @@ class StorageObserverTest {
@Test @Test
fun `zim files are read by the file reader`() = runTest { fun `zim files are read by the file reader`() = runTest {
val expectedBook = val expectedBook =
book( libkiwixBook(
"id", "title", "1", "favicon", "creator", "publisher", "date", "id", "title", "1", "favicon", "creator", "publisher", "date",
"description", "language" "description", "language"
) )

View File

@ -40,13 +40,13 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
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 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.utils.files.testFlow import org.kiwix.kiwixmobile.core.utils.files.testFlow
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.sharedFunctions.book
import org.kiwix.sharedFunctions.bookOnDisk import org.kiwix.sharedFunctions.bookOnDisk
import org.kiwix.sharedFunctions.bookOnDiskEntity import org.kiwix.sharedFunctions.bookOnDiskEntity
import org.kiwix.sharedFunctions.libkiwixBook
import java.io.File import java.io.File
import java.util.concurrent.Callable import java.util.concurrent.Callable
@ -131,9 +131,9 @@ internal class NewBookDaoTest {
fun `insert transaction adds books to the box that have distinct ids`() { fun `insert transaction adds books to the box that have distinct ids`() {
val slot: CapturingSlot<Callable<Unit>> = slot() val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit every { box.store.callInTx(capture(slot)) } returns Unit
val distinctBook: BookOnDisk = bookOnDisk(databaseId = 0, book = book(id = "same")) val distinctBook: BookOnDisk = bookOnDisk(databaseId = 0, book = libkiwixBook(id = "same"))
newBookDao.insert( newBookDao.insert(
listOf(distinctBook, bookOnDisk(databaseId = 1, book = book(id = "same"))) listOf(distinctBook, bookOnDisk(databaseId = 1, book = libkiwixBook(id = "same")))
) )
val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk(relaxed = true) val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk(relaxed = true)
every { box.query() } returns queryBuilder every { box.query() } returns queryBuilder
@ -216,7 +216,7 @@ internal class NewBookDaoTest {
@Test @Test
fun migrationInsert() { fun migrationInsert() {
val book: LibraryNetworkEntity.Book = book() val book: LibkiwixBook = libkiwixBook()
val slot: CapturingSlot<Callable<Unit>> = slot() val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit every { box.store.callInTx(capture(slot)) } returns Unit
newBookDao.migrationInsert(listOf(book)) newBookDao.migrationInsert(listOf(book))

View File

@ -22,16 +22,16 @@ import io.mockk.clearMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
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 java.io.File import java.io.File
class FileUtilsTest { class FileUtilsTest {
private val mockFile: File = mockk() private val mockFile: File = mockk()
private val testBook = Book().apply { file = mockFile } private val testBook = LibkiwixBook().apply { file = mockFile }
private val testId = "8ce5775a-10a9-bbf3-178a-9df69f23263c" private val testId = "8ce5775a-10a9-bbf3-178a-9df69f23263c"
private val fileName = "/data/user/0/org.kiwix.kiwixmobile/files${File.separator}$testId" private val fileName = "/data/user/0/org.kiwix.kiwixmobile/files${File.separator}$testId"

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

View File

@ -22,7 +22,7 @@ import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.downloader.Downloader import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.sharedFunctions.book import org.kiwix.sharedFunctions.libkiwixBook
internal class DownloadCustomTest { internal class DownloadCustomTest {
@Test @Test
@ -35,7 +35,7 @@ internal class DownloadCustomTest {
} }
private fun expectedBook() = private fun expectedBook() =
book( libkiwixBook(
"custom", "custom",
"", "",
"", "",