Fixed: Library items was not showing when applying the language filter.

* Improved the UI to show the selected language on `OnlineLibrary` screen to inform users which language they have selected for fetching the ZIM files.
* Refactored the code to apply the search filter same like kiwix server.
This commit is contained in:
MohitMaliFtechiz 2025-07-08 00:47:46 +05:30
parent 56cfa25d9f
commit c8b17b08e3
5 changed files with 78 additions and 181 deletions

View File

@ -23,6 +23,7 @@ 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.ComposeDimens import org.kiwix.kiwixmobile.core.utils.ComposeDimens
@ -32,9 +33,14 @@ fun HeaderText(
modifier: Modifier, modifier: Modifier,
item: LanguageListItem.HeaderItem item: LanguageListItem.HeaderItem
) { ) {
val context = LocalContext.current
Text( Text(
text = when (item.id) { text = when (item.id) {
LanguageListItem.HeaderItem.SELECTED -> stringResource(R.string.your_language) LanguageListItem.HeaderItem.SELECTED -> stringResource(
R.string.your_language,
context.getString(R.string.empty_string)
)
LanguageListItem.HeaderItem.OTHER -> stringResource(R.string.other_languages) LanguageListItem.HeaderItem.OTHER -> stringResource(R.string.other_languages)
else -> "" else -> ""
}, },

View File

@ -254,7 +254,7 @@ private fun ShowDividerItem(dividerItem: DividerItem) {
.padding(top = SIXTEEN_DP, bottom = EIGHT_DP) .padding(top = SIXTEEN_DP, bottom = EIGHT_DP)
) { ) {
Text( Text(
text = stringResource(dividerItem.stringId), text = dividerItem.sectionTitle,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal) style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal)
) )

View File

@ -49,7 +49,6 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
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
@ -79,22 +78,18 @@ 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.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.LibkiwixBook import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
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
import org.kiwix.kiwixmobile.core.ui.components.TWO import org.kiwix.kiwixmobile.core.ui.components.TWO
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.TAG_KIWIX
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener
import org.kiwix.kiwixmobile.core.zim_manager.ConnectivityBroadcastReceiver import org.kiwix.kiwixmobile.core.zim_manager.ConnectivityBroadcastReceiver
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.fileselect_view.BooksOnDiskListItem import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
@ -123,7 +118,6 @@ import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.Divi
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 retrofit2.Response
import java.util.Locale
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject import javax.inject.Inject
@ -133,7 +127,7 @@ const val MAX_PROGRESS = 100
const val THREE = 3 const val THREE = 3
const val FOUR = 4 const val FOUR = 4
@Suppress("LongParameterList", "LargeClass") @Suppress("LongParameterList", "LargeClass", "UnusedPrivateProperty")
class ZimManageViewModel @Inject constructor( class ZimManageViewModel @Inject constructor(
private val downloadDao: DownloadRoomDao, private val downloadDao: DownloadRoomDao,
private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookOnDisk: LibkiwixBookOnDisk,
@ -181,6 +175,8 @@ class ZimManageViewModel @Inject constructor(
val deviceListScanningProgress = MutableLiveData<Int>() val deviceListScanningProgress = MutableLiveData<Int>()
val libraryListIsRefreshing = MutableLiveData<Boolean>() val libraryListIsRefreshing = MutableLiveData<Boolean>()
private var onlineLibraryFetchingJob: Job? = null
/** /**
* Manages the showing of downloading online library progress, * Manages the showing of downloading online library progress,
* and showing the progressBar at the end of content when loading more items. * and showing the progressBar at the end of content when loading more items.
@ -324,17 +320,16 @@ class ZimManageViewModel @Inject constructor(
private fun observeCoroutineFlows(dispatcher: CoroutineDispatcher = Dispatchers.IO) { private fun observeCoroutineFlows(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
val downloads = downloadDao.downloads() val downloads = downloadDao.downloads()
val booksFromDao = books() val booksFromDao = books()
val selectedLanguage = sharedPreferenceUtil.onlineContentLanguage
coroutineJobs.apply { coroutineJobs.apply {
add(scanBooksFromStorage(dispatcher)) add(scanBooksFromStorage(dispatcher))
add(updateBookItems()) add(updateBookItems())
add(fileSelectActions()) add(fileSelectActions())
add(updateLibraryItems(booksFromDao, downloads, networkLibrary, selectedLanguage)) add(updateLibraryItems(booksFromDao, downloads, networkLibrary))
// add(updateLanguagesInDao(networkLibrary, selectedLanguage))
add(updateNetworkStates()) add(updateNetworkStates())
add(requestsAndConnectivityChangesToLibraryRequests(networkLibrary)) add(requestsAndConnectivityChangesToLibraryRequests(networkLibrary))
add(onlineLibraryRequest()) add(onlineLibraryRequest())
add(observeLanguageChanges()) add(observeLanguageChanges())
add(observeSearch())
} }
} }
@ -348,6 +343,19 @@ class ZimManageViewModel @Inject constructor(
super.onCleared() super.onCleared()
} }
@OptIn(FlowPreview::class)
private fun observeSearch(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
requestFiltering
.onEach {
libraryListIsRefreshing.postValue(true)
updateOnlineLibraryFilters(
OnlineLibraryRequest(query = it, page = ONE, isLoadMoreItem = false)
)
}
.debounce(500)
.flowOn(dispatcher)
.launchIn(viewModelScope)
private fun observeLanguageChanges(dispatcher: CoroutineDispatcher = Dispatchers.IO) = private fun observeLanguageChanges(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
sharedPreferenceUtil.onlineContentLanguage sharedPreferenceUtil.onlineContentLanguage
.onEach { .onEach {
@ -470,34 +478,40 @@ class ZimManageViewModel @Inject constructor(
private fun requestsAndConnectivityChangesToLibraryRequests( private fun requestsAndConnectivityChangesToLibraryRequests(
library: MutableStateFlow<List<LibkiwixBook>>, library: MutableStateFlow<List<LibkiwixBook>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = requestDownloadLibrary.flatMapConcat { onlineLibraryRequest -> ) = requestDownloadLibrary.onEach { onlineLibraryRequest ->
connectivityBroadcastReceiver.networkStates // Cancel any previous ongoing job
.filter { networkState -> networkState == CONNECTED } onlineLibraryFetchingJob?.cancel()
.take(1)
.flatMapConcat { // Launch new job
updateDownloadState(onlineLibraryRequest.isLoadMoreItem.not()) onlineLibraryFetchingJob = viewModelScope.launch(dispatcher) {
shouldProceedWithDownload(onlineLibraryRequest) connectivityBroadcastReceiver.networkStates
.flatMapConcat { kiwixService -> .filter { it == CONNECTED }
downloadLibraryFlow(kiwixService, onlineLibraryRequest) .take(1)
.flatMapConcat {
updateDownloadState(!onlineLibraryRequest.isLoadMoreItem)
shouldProceedWithDownload(onlineLibraryRequest)
.flatMapConcat { kiwixService ->
downloadLibraryFlow(kiwixService, onlineLibraryRequest)
}
}
.filterNotNull()
.catch {
it.printStackTrace()
resetDownloadState()
if (library.value.isEmpty()) {
library.emit(emptyList())
} }
} }
} .collect { result ->
.filterNotNull() library.value = if (result.onlineLibraryRequest.isLoadMoreItem) {
.catch { library.value + result.books
it.printStackTrace().also { } else {
resetDownloadState() result.books
library.emit(emptyList()) }
} resetDownloadState()
}
} }
.onEach { result -> }.flowOn(dispatcher)
networkLibrary.value = if (result.onlineLibraryRequest.isLoadMoreItem) {
networkLibrary.value + result.books
} else {
result.books
}
resetDownloadState()
}
.flowOn(dispatcher)
.launchIn(viewModelScope) .launchIn(viewModelScope)
private fun shouldProceedWithDownload(onlineLibraryRequest: OnlineLibraryRequest): Flow<KiwixService> { private fun shouldProceedWithDownload(onlineLibraryRequest: OnlineLibraryRequest): Flow<KiwixService> {
@ -597,37 +611,22 @@ class ZimManageViewModel @Inject constructor(
localBooksFromLibkiwix: Flow<List<Book>>, localBooksFromLibkiwix: Flow<List<Book>>,
downloads: Flow<List<DownloadModel>>, downloads: Flow<List<DownloadModel>>,
library: MutableStateFlow<List<LibkiwixBook>>, library: MutableStateFlow<List<LibkiwixBook>>,
languages: StateFlow<String>,
dispatcher: CoroutineDispatcher = Dispatchers.IO dispatcher: CoroutineDispatcher = Dispatchers.IO
) = viewModelScope.launch(dispatcher) { ) = viewModelScope.launch(dispatcher) {
val requestFilteringFlow = merge(
flowOf(""),
requestFiltering
.onEach { libraryListIsRefreshing.postValue(true) }
.debounce(500)
.flowOn(dispatcher)
)
combine( combine(
localBooksFromLibkiwix, localBooksFromLibkiwix,
downloads, downloads,
languages.filter { it.isNotEmpty() },
library, library,
requestFilteringFlow,
fat32Checker.fileSystemStates fat32Checker.fileSystemStates
) { args -> ) { args ->
val books = args[ZERO] as List<Book> val books = args[ZERO] as List<Book>
val activeDownloads = args[ONE] as List<DownloadModel> val activeDownloads = args[ONE] as List<DownloadModel>
val languageList = args[TWO] as List<Language> val libraryNetworkEntity = args[TWO] as List<LibkiwixBook>
val libraryNetworkEntity = args[THREE] as List<LibkiwixBook> val fileSystemState = args[THREE] as FileSystemState
val filter = args[FOUR] as String
val fileSystemState = args[FIVE] as FileSystemState
combineLibrarySources( combineLibrarySources(
booksOnFileSystem = books, booksOnFileSystem = books,
activeDownloads = activeDownloads, activeDownloads = activeDownloads,
allLanguages = languageList,
onlineBooks = libraryNetworkEntity, onlineBooks = libraryNetworkEntity,
filter = filter,
fileSystemState = fileSystemState fileSystemState = fileSystemState
) )
} }
@ -640,138 +639,41 @@ class ZimManageViewModel @Inject constructor(
.collect { _libraryItems.emit(it) } .collect { _libraryItems.emit(it) }
} }
// private fun updateLanguagesInDao(
// library: MutableStateFlow<List<LibkiwixBook>>,
// languages: StateFlow<String>,
// dispatcher: CoroutineDispatcher = Dispatchers.IO
// ) =
// combine(
// library,
// languages
// ) { books, existingLanguages ->
// combineToLanguageList(books, existingLanguages)
// }.map { it.sortedBy(Language::language) }
// .filter { it.isNotEmpty() }
// .distinctUntilChanged()
// .catch { it.printStackTrace() }
// .onEach {
// // languageRoomDao.insert(it)
// }
// .flowOn(dispatcher)
// .launchIn(viewModelScope)
private suspend fun combineToLanguageList(
booksFromNetwork: List<LibkiwixBook>,
allLanguages: List<Language>
) = when {
booksFromNetwork.isEmpty() -> {
if (allLanguages.isEmpty()) {
defaultLanguage()
} else {
emptyList()
}
}
allLanguages.isEmpty() ->
fromLocalesWithNetworkMatchesSetActiveBy(
networkLanguageCounts(booksFromNetwork),
defaultLanguage()
)
else ->
fromLocalesWithNetworkMatchesSetActiveBy(
networkLanguageCounts(booksFromNetwork),
allLanguages
)
}
private fun networkLanguageCounts(booksFromNetwork: List<LibkiwixBook>) =
booksFromNetwork.map { it.language }
.fold(
mutableMapOf<String, Int>()
) { acc, language -> acc.increment(language) }
private fun <K> MutableMap<K, Int>.increment(key: K) =
apply { set(key, getOrElse(key) { 0 } + 1) }
private suspend fun fromLocalesWithNetworkMatchesSetActiveBy(
networkLanguageCounts: MutableMap<String, Int>,
listToActivateBy: List<Language>
) = onlineLibraryManager.getOnlineBooksLanguage()
.mapNotNull { code ->
runCatching {
val locale = code.convertToLocal()
val o3 = locale.isO3Language
if (networkLanguageCounts.containsKey(o3)) {
Language(
o3,
languageIsActive(listToActivateBy, locale),
networkLanguageCounts.getOrElse(o3) { 0 }
)
} else {
null
}
}.onFailure {
Log.w(TAG_KIWIX, "Unsupported locale code: $code", it)
}.getOrNull()
}
.distinctBy { it.language }
private fun defaultLanguage() =
listOf(
defaultLanguageProvider.provide()
)
private fun languageIsActive(
allLanguages: List<Language>,
locale: Locale
) = allLanguages.firstOrNull { it.languageCode == locale.isO3Language }?.active == true
@Suppress("UnsafeCallOnNullableType") @Suppress("UnsafeCallOnNullableType")
private fun combineLibrarySources( private fun combineLibrarySources(
booksOnFileSystem: List<Book>, booksOnFileSystem: List<Book>,
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
allLanguages: List<Language>,
onlineBooks: List<LibkiwixBook>, onlineBooks: List<LibkiwixBook>,
filter: String,
fileSystemState: FileSystemState fileSystemState: FileSystemState
): List<LibraryListItem> { ): List<LibraryListItem> {
val activeLanguageCodes =
allLanguages.filter(Language::active)
.map(Language::languageCode)
val allBooks = onlineBooks - booksOnFileSystem.map { LibkiwixBook(it) }.toSet() val allBooks = onlineBooks - booksOnFileSystem.map { LibkiwixBook(it) }.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 }
} }
val booksUnfilteredByLanguage = val filteredBooks = allBooks - downloadingBooks.toSet()
applySearchFilter( val selectedLanguage = sharedPreferenceUtil.selectedOnlineContentLanguage
allBooks - downloadingBooks.toSet(), val onlineLibrarySectionTitle =
filter if (selectedLanguage.isBlank()) {
) context.getString(R.string.all_languages)
} else {
val booksWithActiveLanguages = context.getString(
booksUnfilteredByLanguage.filter { activeLanguageCodes.contains(it.language) } R.string.your_language,
val booksWithoutActiveLanguages = booksUnfilteredByLanguage - booksWithActiveLanguages.toSet() selectedLanguage.convertToLocal().displayLanguage
)
}
return createLibrarySection( return createLibrarySection(
downloadingBooks, downloadingBooks,
activeDownloads, activeDownloads,
fileSystemState, fileSystemState,
R.string.downloading, context.getString(R.string.downloading),
Long.MAX_VALUE Long.MAX_VALUE
) + ) +
createLibrarySection( createLibrarySection(
booksWithActiveLanguages, filteredBooks,
emptyList(), emptyList(),
fileSystemState, fileSystemState,
R.string.your_languages, onlineLibrarySectionTitle,
Long.MAX_VALUE - 1
) +
createLibrarySection(
booksWithoutActiveLanguages,
emptyList(),
fileSystemState,
R.string.other_languages,
Long.MIN_VALUE Long.MIN_VALUE
) )
} }
@ -780,26 +682,16 @@ class ZimManageViewModel @Inject constructor(
books: List<LibkiwixBook>, books: List<LibkiwixBook>,
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
fileSystemState: FileSystemState, fileSystemState: FileSystemState,
sectionStringId: Int, sectionTitle: String,
sectionId: Long sectionId: Long
) = ) =
if (books.isNotEmpty()) { if (books.isNotEmpty()) {
listOf(DividerItem(sectionId, sectionStringId)) + listOf(DividerItem(sectionId, sectionTitle)) +
books.asLibraryItems(activeDownloads, fileSystemState) books.asLibraryItems(activeDownloads, fileSystemState)
} else { } else {
emptyList() emptyList()
} }
private fun applySearchFilter(
unDownloadedBooks: List<LibkiwixBook>,
filter: String
) = if (filter.isEmpty()) {
unDownloadedBooks
} else {
unDownloadedBooks.iterator().forEach { it.calculateSearchMatches(filter, bookUtils) }
unDownloadedBooks.filter { it.searchMatches > 0 }
}
private fun List<LibkiwixBook>.asLibraryItems( private fun List<LibkiwixBook>.asLibraryItems(
activeDownloads: List<DownloadModel>, activeDownloads: List<DownloadModel>,
fileSystemState: FileSystemState fileSystemState: FileSystemState

View File

@ -18,7 +18,6 @@
package org.kiwix.kiwixmobile.zimManager.libraryView.adapter package org.kiwix.kiwixmobile.zimManager.libraryView.adapter
import androidx.annotation.StringRes
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
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
@ -37,7 +36,7 @@ sealed class LibraryListItem {
data class DividerItem constructor( data class DividerItem constructor(
override val id: Long, override val id: Long,
@StringRes val stringId: Int val sectionTitle: String
) : LibraryListItem() ) : LibraryListItem()
data class BookItem constructor( data class BookItem constructor(

View File

@ -180,7 +180,7 @@
<string name="external_link_popup_dialog_message">You are entering an external link. This could lead to additional costs for data transfer or will just not work when you are offline.\nDo you want to continue?</string> <string name="external_link_popup_dialog_message">You are entering an external link. This could lead to additional costs for data transfer or will just not work when you are offline.\nDo you want to continue?</string>
<string name="do_not_ask_anymore">Do not ask anymore</string> <string name="do_not_ask_anymore">Do not ask anymore</string>
<string name="your_languages" tools:keep="@string/your_languages">Selected languages:</string> <string name="your_languages" tools:keep="@string/your_languages">Selected languages:</string>
<string name="your_language" tools:keep="@string/your_language">Selected language:</string> <string name="your_language" tools:keep="@string/your_language">Selected language: %s</string>
<string name="other_languages" tools:keep="@string/other_languages">Other languages:</string> <string name="other_languages" tools:keep="@string/other_languages">Other languages:</string>
<string name="downloading" tools:keep="@string/no_items_msg">Downloading:</string> <string name="downloading" tools:keep="@string/no_items_msg">Downloading:</string>
<string name="no_items_msg" tools:keep="@string/no_items_msg">No items available</string> <string name="no_items_msg" tools:keep="@string/no_items_msg">No items available</string>