Refactored the ZimManageViewModel to fetch the library reactively from the network and laid the groundwork for applying filters in network requests.

This commit is contained in:
MohitMaliFtechiz 2025-07-04 00:47:20 +05:30
parent d9a10b30a6
commit 8b02f96c1c
5 changed files with 284 additions and 84 deletions

View File

@ -57,6 +57,7 @@ import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.ITEMS_PER_PAGE
import org.kiwix.kiwixmobile.core.downloader.Downloader import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.hasNotificationPermission import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.hasNotificationPermission
@ -76,6 +77,7 @@ import org.kiwix.kiwixmobile.core.navigateToAppSettings
import org.kiwix.kiwixmobile.core.navigateToSettings import org.kiwix.kiwixmobile.core.navigateToSettings
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.components.ONE
import org.kiwix.kiwixmobile.core.ui.components.rememberBottomNavigationVisibility import org.kiwix.kiwixmobile.core.ui.components.rememberBottomNavigationVisibility
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem import org.kiwix.kiwixmobile.core.ui.models.IconItem
@ -89,11 +91,13 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.core.zim_manager.NetworkState import org.kiwix.kiwixmobile.core.zim_manager.NetworkState
import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.storage.STORAGE_SELECT_STORAGE_TITLE_TEXTVIEW_SIZE import org.kiwix.kiwixmobile.storage.STORAGE_SELECT_STORAGE_TITLE_TEXTVIEW_SIZE
import org.kiwix.kiwixmobile.storage.StorageSelectDialog import org.kiwix.kiwixmobile.storage.StorageSelectDialog
import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel
import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.OnlineLibraryRequest
import org.kiwix.kiwixmobile.zimManager.libraryView.AvailableSpaceCalculator import org.kiwix.kiwixmobile.zimManager.libraryView.AvailableSpaceCalculator
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import javax.inject.Inject import javax.inject.Inject
@ -138,11 +142,37 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
isSearchActive = false, isSearchActive = false,
searchText = "", searchText = "",
searchValueChangedListener = { onSearchValueChanged(it) }, searchValueChangedListener = { onSearchValueChanged(it) },
clearSearchButtonClickListener = { onSearchClear() } clearSearchButtonClickListener = { onSearchClear() },
onLoadMore = { totalItemShowingCount ->
loadMoreBooksFromNetwork(totalItemShowingCount)
},
isLoadingMoreItem = false
) )
) )
} }
private fun loadMoreBooksFromNetwork(totalItemShowingCount: Int) {
val totalResults = zimManageViewModel.onlineLibraryManager.totalResult
val totalPages =
zimManageViewModel.onlineLibraryManager.calculateTotalPages(
totalResults,
ITEMS_PER_PAGE
)
val currentPage = totalItemShowingCount / ITEMS_PER_PAGE
val nextPage = currentPage + ONE
if (nextPage < totalPages) {
zimManageViewModel.updateOnlineLibraryFilters(
zimManageViewModel.onlineLibraryRequest.value.copy(
page = nextPage,
isLoadMoreItem = true
)
)
} else {
Log.d("OnlineLibrary", "All pages loaded")
}
}
private fun onSearchClear() { private fun onSearchClear() {
onlineLibraryScreenState.value.update { onlineLibraryScreenState.value.update {
copy(searchText = "") copy(searchText = "")
@ -250,9 +280,17 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
} }
observeViewModelData() observeViewModelData()
showPreviouslySearchedTextInSearchView() showPreviouslySearchedTextInSearchView()
startDownloadingLibrary() startDownloadingLibrary(getOnlineLibraryRequest())
} }
private fun getOnlineLibraryRequest(): OnlineLibraryRequest = OnlineLibraryRequest(
null,
null,
null,
false,
1
)
private fun observeViewModelData() { private fun observeViewModelData() {
zimManageViewModel.apply { zimManageViewModel.apply {
// Observe when library items changes. // Observe when library items changes.
@ -265,12 +303,13 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
} }
// Observe when online library downloading. // Observe when online library downloading.
onlineLibraryDownloading onlineLibraryDownloading
.onEach { .onEach { (initialLibraryDownloading, loadingMoreItem) ->
if (it) { if (initialLibraryDownloading) {
showProgressBarOfFetchingOnlineLibrary() showProgressBarOfFetchingOnlineLibrary()
} else { } else {
hideProgressBarOfFetchingOnlineLibrary() hideProgressBarOfFetchingOnlineLibrary()
} }
onlineLibraryScreenState.value.update { copy(isLoadingMoreItem = loadingMoreItem) }
}.launchIn(viewLifecycleOwner.lifecycleScope) }.launchIn(viewLifecycleOwner.lifecycleScope)
// Observe when library list refreshing e.g. applying filters. // Observe when library list refreshing e.g. applying filters.
libraryListIsRefreshing.observe( libraryListIsRefreshing.observe(
@ -369,7 +408,7 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
// User allowed downloading over mobile data. // User allowed downloading over mobile data.
// Since the download flow now triggers only when appropriate, // Since the download flow now triggers only when appropriate,
// we start the library download explicitly after updating the preference. // we start the library download explicitly after updating the preference.
startDownloadingLibrary(true) startDownloadingLibrary(getOnlineLibraryRequest())
}, },
{ {
context.toast( context.toast(
@ -456,18 +495,15 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
NetworkState.CONNECTED -> { NetworkState.CONNECTED -> {
when { when {
NetworkUtils.isWiFi(requireContext()) -> { NetworkUtils.isWiFi(requireContext()) -> {
if (!zimManageViewModel.isOnlineLibraryDownloading) {
refreshFragment(false) refreshFragment(false)
} }
}
noWifiWithWifiOnlyPreferenceSet -> { noWifiWithWifiOnlyPreferenceSet -> {
hideRecyclerviewAndShowSwipeDownForLibraryErrorText() hideRecyclerviewAndShowSwipeDownForLibraryErrorText()
} }
onlineLibraryScreenState.value.value.onlineLibraryList.isNullOrEmpty() && onlineLibraryScreenState.value.value.onlineLibraryList.isNullOrEmpty() -> {
!zimManageViewModel.isOnlineLibraryDownloading -> { startDownloadingLibrary(getOnlineLibraryRequest())
startDownloadingLibrary(true)
showProgressBarOfFetchingOnlineLibrary() showProgressBarOfFetchingOnlineLibrary()
} }
} }
@ -525,15 +561,15 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
if (isNotConnected) { if (isNotConnected) {
showNoInternetConnectionError() showNoInternetConnectionError()
} else { } else {
startDownloadingLibrary(isExplicitRefresh) startDownloadingLibrary(getOnlineLibraryRequest())
if (isExplicitRefresh) { if (isExplicitRefresh) {
showRecyclerviewAndHideSwipeDownForLibraryErrorText() showRecyclerviewAndHideSwipeDownForLibraryErrorText()
} }
} }
} }
private fun startDownloadingLibrary(isExplicitRefresh: Boolean = false) { private fun startDownloadingLibrary(onlineLibraryRequest: OnlineLibraryRequest) {
zimManageViewModel.requestOnlineLibraryIfNeeded(isExplicitRefresh) zimManageViewModel.updateOnlineLibraryFilters(onlineLibraryRequest)
} }
private fun downloadFile() { private fun downloadFile() {

View File

@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -42,6 +43,9 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -51,7 +55,11 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.downloader.downloadManager.FIVE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll
import org.kiwix.kiwixmobile.core.main.reader.rememberScrollBehavior import org.kiwix.kiwixmobile.core.main.reader.rememberScrollBehavior
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
@ -194,6 +202,46 @@ private fun OnlineLibraryList(state: OnlineLibraryScreenState, lazyListState: La
} }
} }
} }
showLoadMoreProgressBar(state.isLoadingMoreItem)
}
LaunchedEffect(state.isLoadingMoreItem) {
if (state.isLoadingMoreItem) {
// Scroll to the last item (i.e., the loading spinner)
val lastItemIndex = state.onlineLibraryList?.size ?: 0
lazyListState.animateScrollToItem(lastItemIndex)
}
}
LaunchedEffect(lazyListState) {
snapshotFlow {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
val totalItems = layoutInfo.totalItemsCount
val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: ZERO
(totalItems > ZERO && lastVisibleItemIndex >= (totalItems - FIVE)) to totalItems
}.value
}
.distinctUntilChanged()
.filter { it.first }
.collect { (_, totalItems) ->
state.onLoadMore(totalItems)
}
}
}
private fun LazyListScope.showLoadMoreProgressBar(isLoadingMoreItem: Boolean) {
if (isLoadingMoreItem) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(SIXTEEN_DP),
contentAlignment = Alignment.Center
) {
ContentLoadingProgressBar()
}
}
} }
} }

View File

@ -99,5 +99,13 @@ data class OnlineLibraryScreenState(
/** /**
* Triggers when clear button clicked. * Triggers when clear button clicked.
*/ */
val clearSearchButtonClickListener: () -> Unit val clearSearchButtonClickListener: () -> Unit,
/**
* Triggers when user at the end of the online content.
*/
val onLoadMore: (Int) -> Unit,
/**
* Manages the showing of progressBar at the end of book list when more items is loading.
*/
val isLoadingMoreItem: Boolean
) )

View File

@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.zimManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.ITEMS_PER_PAGE
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.OPDS_LIBRARY_ENDPOINT import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.OPDS_LIBRARY_ENDPOINT
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
@ -44,7 +45,8 @@ class OnlineLibraryManager @Inject constructor(
urlHost: String urlHost: String
): ArrayList<LibkiwixBook>? = ): ArrayList<LibkiwixBook>? =
runCatching { runCatching {
content?.let { totalResult = extractTotalResults(it) } if (content == null) return null
totalResult = extractTotalResults(content)
val onlineBooksList = arrayListOf<LibkiwixBook>() val onlineBooksList = arrayListOf<LibkiwixBook>()
val tempLibrary = Library() val tempLibrary = Library()
val tempManager = Manager(tempLibrary) val tempManager = Manager(tempLibrary)
@ -83,8 +85,8 @@ class OnlineLibraryManager @Inject constructor(
*/ */
fun buildLibraryUrl( fun buildLibraryUrl(
baseUrl: String, baseUrl: String,
start: Int = 0, start: Int,
count: Int = 50, count: Int,
query: String? = null, query: String? = null,
lang: String? = null, lang: String? = null,
category: String? = null category: String? = null

View File

@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -53,6 +54,7 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.retry import kotlinx.coroutines.flow.retry
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -71,7 +73,6 @@ import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk
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.ITEMS_PER_PAGE import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.ITEMS_PER_PAGE
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.OPDS_LIBRARY_ENDPOINT
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
@ -123,6 +124,7 @@ import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.Book
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 org.kiwix.libkiwix.Book import org.kiwix.libkiwix.Book
import retrofit2.Response
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
@ -148,7 +150,7 @@ class ZimManageViewModel @Inject constructor(
private val dataSource: DataSource, private val dataSource: DataSource,
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
private val sharedPreferenceUtil: SharedPreferenceUtil, private val sharedPreferenceUtil: SharedPreferenceUtil,
private val onlineLibraryManager: OnlineLibraryManager val onlineLibraryManager: OnlineLibraryManager
) : ViewModel() { ) : ViewModel() {
sealed class FileSelectActions { sealed class FileSelectActions {
data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions() data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions()
@ -161,6 +163,19 @@ class ZimManageViewModel @Inject constructor(
object UserClickedDownloadBooksButton : FileSelectActions() object UserClickedDownloadBooksButton : FileSelectActions()
} }
data class OnlineLibraryRequest(
val query: String?,
val category: String?,
val lang: String?,
val isLoadMoreItem: Boolean,
val page: Int
)
data class OnlineLibraryResult(
val onlineLibraryRequest: OnlineLibraryRequest,
val books: List<LibkiwixBook>
)
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,20 +183,36 @@ class ZimManageViewModel @Inject constructor(
val fileSelectListStates: MutableLiveData<FileSelectListState> = MutableLiveData() val fileSelectListStates: MutableLiveData<FileSelectListState> = MutableLiveData()
val deviceListScanningProgress = MutableLiveData<Int>() val deviceListScanningProgress = MutableLiveData<Int>()
val libraryListIsRefreshing = MutableLiveData<Boolean>() val libraryListIsRefreshing = MutableLiveData<Boolean>()
val onlineLibraryDownloading = MutableStateFlow(false)
/**
* Manages the showing of downloading online library progress,
* and showing the progressBar at the end of content when loading more items.
*
* A [Pair] containing:
* - [Boolean]: When initial content is downloading.
* - [Boolean]: When loading more item.
*/
val onlineLibraryDownloading = MutableStateFlow(false to false)
val shouldShowWifiOnlyDialog = MutableLiveData<Boolean>() val shouldShowWifiOnlyDialog = MutableLiveData<Boolean>()
val networkStates = MutableLiveData<NetworkState>() val networkStates = MutableLiveData<NetworkState>()
val networkLibrary = MutableSharedFlow<List<LibkiwixBook>>(replay = 0) val networkLibrary = MutableStateFlow<List<LibkiwixBook>>(emptyList())
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<OnlineLibraryRequest>(
replay = 0, replay = 0,
extraBufferCapacity = 1, extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
val onlineLibraryRequest: MutableStateFlow<OnlineLibraryRequest> =
@Volatile MutableStateFlow<OnlineLibraryRequest>(
var isOnlineLibraryDownloading = false OnlineLibraryRequest(
query = null,
category = null,
lang = null,
isLoadMoreItem = false,
page = 0
)
)
val requestFiltering = MutableStateFlow("") val requestFiltering = MutableStateFlow("")
val onlineBooksSearchedQuery = MutableLiveData<String>() val onlineBooksSearchedQuery = MutableLiveData<String>()
private val coroutineJobs: MutableList<Job> = mutableListOf() private val coroutineJobs: MutableList<Job> = mutableListOf()
@ -194,14 +225,6 @@ class ZimManageViewModel @Inject constructor(
context.registerReceiver(connectivityBroadcastReceiver) context.registerReceiver(connectivityBroadcastReceiver)
} }
fun requestOnlineLibraryIfNeeded(isExplicitRefresh: Boolean) {
if (isOnlineLibraryDownloading && !isExplicitRefresh) return
isOnlineLibraryDownloading = true
viewModelScope.launch {
requestDownloadLibrary.tryEmit(Unit)
}
}
fun setIsUnitTestCase() { fun setIsUnitTestCase() {
isUnitTestCase = true isUnitTestCase = true
} }
@ -210,9 +233,18 @@ class ZimManageViewModel @Inject constructor(
this.alertDialogShower = alertDialogShower this.alertDialogShower = alertDialogShower
} }
private fun createKiwixServiceWithProgressListener(): KiwixService { private fun createKiwixServiceWithProgressListener(
baseUrl: String,
start: Int = ZERO,
count: Int = ITEMS_PER_PAGE,
query: String? = null,
lang: String? = null,
category: String? = null,
shouldTrackProgress: Boolean
): KiwixService {
if (isUnitTestCase) return kiwixService if (isUnitTestCase) return kiwixService
val contentLength = getContentLengthOfLibraryXmlFile() val contentLength =
getContentLengthOfLibraryXmlFile(baseUrl, start, count, query, lang, category)
val customOkHttpClient = val customOkHttpClient =
OkHttpClient().newBuilder() OkHttpClient().newBuilder()
.followRedirects(true) .followRedirects(true)
@ -228,22 +260,19 @@ class ZimManageViewModel @Inject constructor(
.addNetworkInterceptor(UserAgentInterceptor(USER_AGENT)) .addNetworkInterceptor(UserAgentInterceptor(USER_AGENT))
.addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request()) val originalResponse = chain.proceed(chain.request())
originalResponse.body?.let { responseBody -> val body = originalResponse.body
if (shouldTrackProgress && body != null) {
originalResponse.newBuilder() originalResponse.newBuilder()
.body( .body(ProgressResponseBody(body, appProgressListener, contentLength))
ProgressResponseBody(
responseBody,
appProgressListener,
contentLength
)
)
.build() .build()
} ?: originalResponse } else {
originalResponse
}
} }
.build() .build()
return KiwixService.ServiceCreator.newHackListService( return KiwixService.ServiceCreator.newHackListService(
customOkHttpClient, customOkHttpClient,
KIWIX_OPDS_LIBRARY_URL baseUrl
) )
.also { .also {
kiwixService = it kiwixService = it
@ -252,10 +281,19 @@ class ZimManageViewModel @Inject constructor(
private var appProgressListener: AppProgressListenerProvider? = AppProgressListenerProvider(this) private var appProgressListener: AppProgressListenerProvider? = AppProgressListenerProvider(this)
private fun getContentLengthOfLibraryXmlFile(): Long { private fun getContentLengthOfLibraryXmlFile(
baseUrl: String,
start: Int = ZERO,
count: Int = ITEMS_PER_PAGE,
query: String? = null,
lang: String? = null,
category: String? = null
): Long {
val requestUrl =
onlineLibraryManager.buildLibraryUrl(baseUrl, start, count, query, lang, category)
val headRequest = val headRequest =
Request.Builder() Request.Builder()
.url("$KIWIX_OPDS_LIBRARY_URL$OPDS_LIBRARY_ENDPOINT?count=$ITEMS_PER_PAGE") .url(requestUrl)
.head() .head()
.header("Accept-Encoding", "identity") .header("Accept-Encoding", "identity")
.build() .build()
@ -298,6 +336,7 @@ class ZimManageViewModel @Inject constructor(
add(updateLanguagesInDao(networkLibrary, languages)) add(updateLanguagesInDao(networkLibrary, languages))
add(updateNetworkStates()) add(updateNetworkStates())
add(requestsAndConnectivityChangesToLibraryRequests(networkLibrary)) add(requestsAndConnectivityChangesToLibraryRequests(networkLibrary))
add(onlineLibraryRequest())
} }
} }
@ -311,6 +350,25 @@ class ZimManageViewModel @Inject constructor(
super.onCleared() super.onCleared()
} }
fun updateOnlineLibraryFilters(newRequest: OnlineLibraryRequest) {
onlineLibraryRequest.update { current ->
current.copy(
query = newRequest.query.takeUnless { it.isNullOrEmpty() } ?: current.query,
category = newRequest.category.takeUnless { it.isNullOrEmpty() } ?: current.category,
lang = newRequest.lang.takeUnless { it.isNullOrEmpty() } ?: current.lang,
page = newRequest.page,
isLoadMoreItem = newRequest.isLoadMoreItem
)
}
}
private fun onlineLibraryRequest() = onlineLibraryRequest
.drop(1)
.onEach { request ->
requestDownloadLibrary.tryEmit(request)
}
.launchIn(viewModelScope)
private fun scanBooksFromStorage(dispatcher: CoroutineDispatcher = Dispatchers.IO) = private fun scanBooksFromStorage(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
checkFileSystemForBooksOnRequest(books()) checkFileSystemForBooksOnRequest(books())
.catch { it.printStackTrace() } .catch { it.printStackTrace() }
@ -392,85 +450,133 @@ class ZimManageViewModel @Inject constructor(
return None return None
} }
private fun updateDownloadState(isInitial: Boolean) {
onlineLibraryDownloading.tryEmit(isInitial to !isInitial)
}
private fun resetDownloadState() {
onlineLibraryDownloading.tryEmit(false to false)
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun requestsAndConnectivityChangesToLibraryRequests( private fun requestsAndConnectivityChangesToLibraryRequests(
library: MutableSharedFlow<List<LibkiwixBook>>, library: MutableStateFlow<List<LibkiwixBook>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = requestDownloadLibrary.flatMapConcat { ) = requestDownloadLibrary.flatMapConcat { onlineLibraryRequest ->
connectivityBroadcastReceiver.networkStates connectivityBroadcastReceiver.networkStates
.filter { networkState -> networkState == CONNECTED } .filter { networkState -> networkState == CONNECTED }
.take(1) .take(1)
.flatMapConcat { .flatMapConcat {
shouldProceedWithDownload() updateDownloadState(onlineLibraryRequest.isLoadMoreItem.not())
shouldProceedWithDownload(onlineLibraryRequest)
.flatMapConcat { kiwixService -> .flatMapConcat { kiwixService ->
downloadLibraryFlow(kiwixService).also { downloadLibraryFlow(kiwixService, onlineLibraryRequest)
onlineLibraryDownloading.tryEmit(true)
}
} }
} }
} }
.filterNotNull() .filterNotNull()
.catch { .catch {
it.printStackTrace().also { it.printStackTrace().also {
isOnlineLibraryDownloading = false resetDownloadState()
onlineLibraryDownloading.tryEmit(false)
library.emit(emptyList()) library.emit(emptyList())
} }
} }
.onEach { .onEach { result ->
library.emit(it).also { networkLibrary.value = if (result.onlineLibraryRequest.isLoadMoreItem) {
// Setting this to true because once library downloaded we don't need to download again networkLibrary.value + result.books
// until user wants to refresh the online library. } else {
isOnlineLibraryDownloading = true result.books
onlineLibraryDownloading.tryEmit(false)
} }
resetDownloadState()
} }
.flowOn(dispatcher) .flowOn(dispatcher)
.launchIn(viewModelScope) .launchIn(viewModelScope)
private fun shouldProceedWithDownload(): Flow<KiwixService> { private fun shouldProceedWithDownload(onlineLibraryRequest: OnlineLibraryRequest): Flow<KiwixService> {
val baseUrl = KIWIX_OPDS_LIBRARY_URL
val start =
onlineLibraryManager.getStartOffset(onlineLibraryRequest.page.minus(ONE), ITEMS_PER_PAGE)
val shouldTrackProgress = !onlineLibraryRequest.isLoadMoreItem
return if (connectivityManager.isWifi()) { return if (connectivityManager.isWifi()) {
flowOf(createKiwixServiceWithProgressListener()) flowOf(
createKiwixServiceWithProgressListener(
baseUrl,
start,
ITEMS_PER_PAGE,
onlineLibraryRequest.query,
onlineLibraryRequest.lang,
onlineLibraryRequest.category,
shouldTrackProgress
)
)
} else { } else {
flow { flow {
val wifiOnly = sharedPreferenceUtil.prefWifiOnlys.first() val wifiOnly = sharedPreferenceUtil.prefWifiOnlys.first()
if (wifiOnly) { if (wifiOnly) {
onlineLibraryDownloading.emit(false to false)
shouldShowWifiOnlyDialog.postValue(true) shouldShowWifiOnlyDialog.postValue(true)
// Don't emit anything — just return // Don't emit anything — just return
return@flow return@flow
} }
emit(createKiwixServiceWithProgressListener()) emit(
createKiwixServiceWithProgressListener(
baseUrl,
start,
ITEMS_PER_PAGE,
onlineLibraryRequest.query,
onlineLibraryRequest.lang,
onlineLibraryRequest.category,
shouldTrackProgress
)
)
} }
} }
} }
private fun downloadLibraryFlow( private fun downloadLibraryFlow(
kiwixService: KiwixService kiwixService: KiwixService,
): Flow<List<LibkiwixBook>> = flow { request: OnlineLibraryRequest
downloadProgress.postValue(context.getString(R.string.starting_downloading_remote_library)) ): Flow<OnlineLibraryResult> = flow {
// TODO get the filter from online library and pass it here to get the online content based on filters. updateDownloadProgressIfNeeded(
request,
R.string.starting_downloading_remote_library
)
val start =
onlineLibraryManager.getStartOffset(request.page.minus(ONE), ITEMS_PER_PAGE)
val buildUrl = onlineLibraryManager.buildLibraryUrl( val buildUrl = onlineLibraryManager.buildLibraryUrl(
KIWIX_OPDS_LIBRARY_URL, KIWIX_OPDS_LIBRARY_URL,
start,
ITEMS_PER_PAGE,
request.query,
request.lang,
request.category,
) )
val response = kiwixService.getLibraryPage(buildUrl) val response = kiwixService.getLibraryPage(buildUrl)
val resolvedUrl = response.raw().networkResponse?.request?.url val urlHost = response.getResolvedBaseUrl()
?: response.raw().request.url updateDownloadProgressIfNeeded(
val baseHostUrl = "${resolvedUrl.scheme}://${resolvedUrl.host}" request,
downloadProgress.postValue(context.getString(R.string.parsing_remote_library)) R.string.parsing_remote_library
val libraryXml = response.body()
val onlineBooks = onlineLibraryManager.parseOPDSStreamAndGetBooks(libraryXml, baseHostUrl)
emit(
if (onlineBooks.isNullOrEmpty()) {
emptyList()
} else {
onlineBooks
}
) )
val libraryXml = response.body()
val onlineBooks =
onlineLibraryManager.parseOPDSStreamAndGetBooks(libraryXml, urlHost).orEmpty()
emit(OnlineLibraryResult(request, onlineBooks))
} }
.retry(5) .retry(5)
.catch { e -> .catch { e ->
e.printStackTrace() e.printStackTrace()
emit(emptyList()) emit(OnlineLibraryResult(request, emptyList()))
}
private fun updateDownloadProgressIfNeeded(request: OnlineLibraryRequest, messageResId: Int) {
if (!request.isLoadMoreItem) {
downloadProgress.postValue(context.getString(messageResId))
}
}
private fun Response<String>.getResolvedBaseUrl(): String {
val url = raw().networkResponse?.request?.url ?: raw().request.url
return "${url.scheme}://${url.host}"
} }
private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates
@ -482,7 +588,7 @@ class ZimManageViewModel @Inject constructor(
private fun updateLibraryItems( private fun updateLibraryItems(
localBooksFromLibkiwix: Flow<List<Book>>, localBooksFromLibkiwix: Flow<List<Book>>,
downloads: Flow<List<DownloadModel>>, downloads: Flow<List<DownloadModel>>,
library: MutableSharedFlow<List<LibkiwixBook>>, library: MutableStateFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>, languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = viewModelScope.launch(dispatcher) { ) = viewModelScope.launch(dispatcher) {
@ -527,7 +633,7 @@ class ZimManageViewModel @Inject constructor(
} }
private fun updateLanguagesInDao( private fun updateLanguagesInDao(
library: MutableSharedFlow<List<LibkiwixBook>>, library: MutableStateFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>, languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = ) =