Migrated BookmarkFragment, HistoryFragment, and NotesFragment to Jetpack Compose.

* Created a `PageScreen` composable, which serves as the base screen for all these fragments. Each fragment can customize it as needed.
* Introduced a reusable `PageListItem` composable for consistent list item rendering.
* Added an extension function to convert the favicon to a Compose ImageBitmap. If no favicon is available, it falls back to the default ZIM icon, maintaining behavior from the XML-based UI.
This commit is contained in:
MohitMaliFtechiz 2025-04-10 19:47:40 +05:30
parent 45724dca02
commit bceebd5de4
10 changed files with 271 additions and 31 deletions

View File

@ -44,7 +44,8 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.faviconToPainter
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.BOOK_ICON_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
@ -54,8 +55,8 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_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.fileselect_view.ArticleCount
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode
const val BOOK_ITEM_CHECKBOX_TESTING_TAG = "bookItemCheckboxTestingTag"
const val BOOK_ITEM_TESTING_TAG = "bookItemTestingTag"
@ -115,7 +116,7 @@ private fun BookContent(
if (selectionMode == SelectionMode.MULTI) {
BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick, index)
}
BookIcon(bookOnDisk.book.faviconToPainter())
BookIcon(Base64String(bookOnDisk.book.favicon).toPainter())
BookDetails(Modifier.weight(1f), bookOnDisk)
}
}

View File

@ -21,6 +21,13 @@ package org.kiwix.kiwixmobile.core.downloader.model
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import org.kiwix.kiwixmobile.core.R
@JvmInline
value class Base64String(private val encodedString: String?) {
@ -35,3 +42,13 @@ value class Base64String(private val encodedString: String?) {
null
}
}
@Composable
fun Base64String.toPainter(): Painter {
val bitmap = remember(this) { toBitmap() }
return if (bitmap != null) {
BitmapPainter(bitmap.asImageBitmap())
} else {
painterResource(id = R.drawable.default_zim_file_icon)
}
}

View File

@ -18,15 +18,7 @@
package org.kiwix.kiwixmobile.core.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.NetworkUtils
@ -62,14 +54,3 @@ fun Book.buildSearchableText(bookUtils: BookUtils): String =
append("|")
}
}.toString()
@Composable
fun Book.faviconToPainter(): Painter {
val base64String = Base64String(favicon)
val bitmap = remember(base64String) { base64String.toBitmap() }
return if (bitmap != null) {
BitmapPainter(bitmap.asImageBitmap())
} else {
painterResource(id = R.drawable.default_zim_file_icon)
}
}

View File

@ -31,6 +31,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle
@ -67,7 +68,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
@Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil
private var actionMode: ActionMode? = null
val compositeDisposable = CompositeDisposable()
abstract val screenTitle: String
abstract val screenTitle: Int
abstract val noItemsString: String
abstract val switchString: String
abstract val searchQueryHint: String
@ -78,7 +79,6 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
override val fragmentToolbar: Toolbar? by lazy {
fragmentPageBinding?.root?.findViewById(R.id.toolbar)
}
override val fragmentTitle: String? by lazy { screenTitle }
private val actionModeCallback: ActionMode.Callback =
object : ActionMode.Callback {
@ -145,7 +145,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupMenu()
// setupMenu()
val activity = requireActivity() as CoreMainActivity
fragmentPageBinding?.recyclerView?.apply {
layoutManager =
@ -182,8 +182,24 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentPageBinding = FragmentPageBinding.inflate(inflater, container, false)
return fragmentPageBinding?.root
return ComposeView(requireContext()).apply {
setContent {
PageScreen(
pageState = pageViewModel.state,
effects = pageViewModel.effects,
screenTitle = screenTitle,
noItemsString = noItemsString,
switchString = switchString,
searchQueryHint = searchQueryHint,
switchIsChecked = switchIsChecked,
onSwitchChanged = { isChecked ->
pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked))
},
onItemClick = { pageViewModel.actions.offer(Action.OnItemClick(it)) },
onItemLongClick = { pageViewModel.actions.offer(Action.OnItemLongClick(it)) }
)
}
}
}
override fun onDestroyView() {

View File

@ -0,0 +1,86 @@
/*
* 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.page
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.toPainter
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PAGE_LIST_ITEM_FAVICON_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PageListItem(
page: Page,
onClick: () -> Unit,
onLongClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.background(MaterialTheme.colorScheme.surface)
.padding(
horizontal = SIXTEEN_DP,
vertical = EIGHT_DP
),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = if (page.isSelected) {
painterResource(id = R.drawable.ic_check_circle_blue_24dp)
} else {
Base64String(page.favicon).toPainter()
},
contentDescription = stringResource(R.string.fav_icon),
modifier = Modifier
.size(PAGE_LIST_ITEM_FAVICON_SIZE)
)
Spacer(modifier = Modifier.width(SIXTEEN_DP))
Text(
text = page.title,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@ -0,0 +1,137 @@
/*
* 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.page
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.MutableLiveData
import io.reactivex.processors.PublishProcessor
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.DateItem
import org.kiwix.kiwixmobile.core.page.viewmodel.PageState
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
@Suppress("LongParameterList", "IgnoredReturnValue", "UnusedParameter")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PageScreen(
pageState: MutableLiveData<out PageState<out Page>>,
effects: PublishProcessor<SideEffect<*>>,
screenTitle: Int,
noItemsString: String,
switchString: String,
searchQueryHint: String,
switchIsChecked: Boolean,
onSwitchChanged: (Boolean) -> Unit,
onItemClick: (Page) -> Unit,
onItemLongClick: (Page) -> Unit
) {
val context = LocalActivity.current as CoreMainActivity
val state by pageState.observeAsState()
LaunchedEffect(Unit) {
effects.subscribe { it.invokeWith(context) }
}
KiwixTheme {
Scaffold(
topBar = {
Column {
KiwixAppBar(
titleId = screenTitle,
navigationIcon = {},
)
if (!context.isCustomApp()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SIXTEEN_DP, vertical = EIGHT_DP),
verticalAlignment = Alignment.CenterVertically
) {
Text(switchString, modifier = Modifier.weight(1f))
Switch(checked = switchIsChecked, onCheckedChange = onSwitchChanged)
}
}
}
}
) { padding ->
val items = state?.visiblePageItems.orEmpty()
Box(modifier = Modifier.padding(padding)) {
if (items.isEmpty()) {
Text(
text = noItemsString,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.align(Alignment.Center)
)
} else {
LazyColumn {
items(items) { item ->
when (item) {
is Page -> {
PageListItem(
page = item,
onClick = { onItemClick(item) },
onLongClick = { onItemLongClick(item) }
)
}
is DateItem -> {
DateItemText(item)
}
}
}
}
}
}
}
}
}
@Composable
fun DateItemText(dateItem: DateItem) {
Text(
text = dateItem.dateString,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(SIXTEEN_DP)
)
}

View File

@ -16,7 +16,7 @@ class BookmarksFragment : PageFragment() {
PageAdapter(PageItemDelegate(this))
}
override val screenTitle: String by lazy { getString(R.string.bookmarks) }
override val screenTitle: Int by lazy { R.string.bookmarks }
override val noItemsString: String by lazy { getString(R.string.no_bookmarks) }
override val switchString: String by lazy { getString(R.string.bookmarks_from_current_book) }
override val deleteIconTitle: String by lazy {

View File

@ -21,7 +21,7 @@ class HistoryFragment : PageFragment() {
override val noItemsString: String by lazy { getString(R.string.no_history) }
override val switchString: String by lazy { getString(R.string.history_from_current_book) }
override val screenTitle: String by lazy { getString(R.string.history) }
override val screenTitle: Int by lazy { R.string.history }
override val deleteIconTitle: String by lazy {
getString(R.string.pref_clear_all_history_title)
}

View File

@ -30,8 +30,7 @@ import org.kiwix.kiwixmobile.core.page.notes.viewmodel.NotesViewModel
class NotesFragment : PageFragment() {
override val pageViewModel by lazy { viewModel<NotesViewModel>(viewModelFactory) }
override val screenTitle: String
get() = getString(R.string.pref_notes)
override val screenTitle: Int get() = R.string.pref_notes
override val pageAdapter: PageAdapter by lazy {
PageAdapter(PageDelegate.PageItemDelegate(this))

View File

@ -106,4 +106,7 @@ object ComposeDimens {
val HELP_SCREEN_ITEM_TITLE_TEXT_SIZE = 20.sp
val HELP_SCREEN_ITEM_TITLE_LETTER_SPACING = 0.0125.em
val HELP_SCREEN_ARROW_ICON_SIZE = 35.dp
// Page dimens
val PAGE_LIST_ITEM_FAVICON_SIZE = 40.dp
}