Migrated the Reader screen to jetpack compose.

* Created the `ReaderScreen` for compose UI.
* Created the `ReaderScreenState` to manage the state of UI.
This commit is contained in:
MohitMaliFtechiz 2025-06-13 00:29:52 +05:30
parent 11d1c161c6
commit 39baa985fb
9 changed files with 432 additions and 48 deletions

View File

@ -41,7 +41,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.kiwix.kiwixmobile.BaseActivityTest import org.kiwix.kiwixmobile.BaseActivityTest
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.main.CoreReaderFragment import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil

View File

@ -55,6 +55,7 @@ import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.R.string import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.main.reader.CONTENT_LOADING_PROGRESSBAR_TESTING_TAG
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixButton import org.kiwix.kiwixmobile.core.ui.components.KiwixButton
@ -78,7 +79,6 @@ import org.kiwix.kiwixmobile.zimManager.fileselectView.FileSelectListState
const val NO_FILE_TEXT_TESTING_TAG = "noFileTextTestingTag" const val NO_FILE_TEXT_TESTING_TAG = "noFileTextTestingTag"
const val DOWNLOAD_BUTTON_TESTING_TAG = "downloadButtonTestingTag" const val DOWNLOAD_BUTTON_TESTING_TAG = "downloadButtonTestingTag"
const val BOOK_LIST_TESTING_TAG = "bookListTestingTag" const val BOOK_LIST_TESTING_TAG = "bookListTestingTag"
const val CONTENT_LOADING_PROGRESSBAR_TESTING_TAG = "contentLoadingProgressBarTestingTag"
const val SELECT_FILE_BUTTON_TESTING_TAG = "selectFileButtonTestingTag" const val SELECT_FILE_BUTTON_TESTING_TAG = "selectFileButtonTestingTag"
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)

View File

@ -50,11 +50,11 @@ import org.kiwix.kiwixmobile.core.extensions.setImageDrawableCompat
import org.kiwix.kiwixmobile.core.extensions.snack import org.kiwix.kiwixmobile.core.extensions.snack
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.CoreReaderFragment import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.CoreWebViewClient import org.kiwix.kiwixmobile.core.main.CoreWebViewClient
import org.kiwix.kiwixmobile.core.main.RestoreOrigin import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin
import org.kiwix.kiwixmobile.core.main.RestoreOrigin.FromExternalLaunch import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromExternalLaunch
import org.kiwix.kiwixmobile.core.main.RestoreOrigin.FromSearchScreen import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromSearchScreen
import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package org.kiwix.kiwixmobile.core.main package org.kiwix.kiwixmobile.core.main.reader
import android.Manifest import android.Manifest
import android.Manifest.permission.POST_NOTIFICATIONS import android.Manifest.permission.POST_NOTIFICATIONS
@ -131,13 +131,33 @@ import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
import org.kiwix.kiwixmobile.core.extensions.showFullScreenMode import org.kiwix.kiwixmobile.core.extensions.showFullScreenMode
import org.kiwix.kiwixmobile.core.extensions.snack import org.kiwix.kiwixmobile.core.extensions.snack
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.main.AddNoteDialog
import org.kiwix.kiwixmobile.core.main.CompatFindActionModeCallback
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.CoreSearchWidget
import org.kiwix.kiwixmobile.core.main.CoreWebViewClient
import org.kiwix.kiwixmobile.core.main.DarkModeViewPainter
import org.kiwix.kiwixmobile.core.main.DocumentParser
import org.kiwix.kiwixmobile.core.main.DocumentParser.SectionsListener import org.kiwix.kiwixmobile.core.main.DocumentParser.SectionsListener
import org.kiwix.kiwixmobile.core.main.FIND_IN_PAGE_SEARCH_STRING
import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech
import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnInitSucceedListener import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnInitSucceedListener
import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnSpeakingListener import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnSpeakingListener
import org.kiwix.kiwixmobile.core.main.KiwixWebView
import org.kiwix.kiwixmobile.core.main.MainMenu
import org.kiwix.kiwixmobile.core.main.MainMenu.MenuClickListener import org.kiwix.kiwixmobile.core.main.MainMenu.MenuClickListener
import org.kiwix.kiwixmobile.core.main.RestoreOrigin.FromExternalLaunch import org.kiwix.kiwixmobile.core.main.MainRepositoryActions
import org.kiwix.kiwixmobile.core.main.OnSwipeTouchListener
import org.kiwix.kiwixmobile.core.main.ServiceWorkerUninitialiser
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromExternalLaunch
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.TableClickListener import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.TableClickListener
import org.kiwix.kiwixmobile.core.main.TabsAdapter
import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView
import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS
import org.kiwix.kiwixmobile.core.main.WebViewCallback
import org.kiwix.kiwixmobile.core.main.WebViewProvider
import org.kiwix.kiwixmobile.core.navigateToAppSettings import org.kiwix.kiwixmobile.core.navigateToAppSettings
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.page.history.NavigationHistoryClickListener import org.kiwix.kiwixmobile.core.page.history.NavigationHistoryClickListener
@ -824,9 +844,9 @@ abstract class CoreReaderFragment :
// the unwanted blank space caused by the toolbar. // the unwanted blank space caused by the toolbar.
setTopMarginToWebViews(-requireActivity().getToolbarHeight()) setTopMarginToWebViews(-requireActivity().getToolbarHeight())
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
bottomToolbar?.visibility = View.GONE bottomToolbar?.visibility = GONE
contentFrame?.visibility = View.GONE contentFrame?.visibility = GONE
progressBar?.visibility = View.GONE progressBar?.visibility = GONE
backToTopButton?.hide() backToTopButton?.hide()
setTabSwitcherVisibility(VISIBLE) setTabSwitcherVisibility(VISIBLE)
startAnimation(tabSwitcherRoot, R.anim.slide_down) startAnimation(tabSwitcherRoot, R.anim.slide_down)
@ -906,10 +926,10 @@ abstract class CoreReaderFragment :
) )
tabSwitcherRoot?.let { tabSwitcherRoot?.let {
if (it.isVisible) { if (it.isVisible) {
setTabSwitcherVisibility(View.GONE) setTabSwitcherVisibility(GONE)
startAnimation(it, R.anim.slide_up) startAnimation(it, R.anim.slide_up)
progressBar?.visibility = View.VISIBLE progressBar?.visibility = VISIBLE
contentFrame?.visibility = View.VISIBLE contentFrame?.visibility = VISIBLE
} }
} }
progressBar?.hide() progressBar?.hide()
@ -1058,7 +1078,7 @@ abstract class CoreReaderFragment :
@Suppress("ReturnCount", "NestedBlockDepth") @Suppress("ReturnCount", "NestedBlockDepth")
override fun onBackPressed(activity: AppCompatActivity): FragmentActivityExtensions.Super { override fun onBackPressed(activity: AppCompatActivity): FragmentActivityExtensions.Super {
when { when {
tabSwitcherRoot?.visibility == View.VISIBLE -> { tabSwitcherRoot?.visibility == VISIBLE -> {
selectTab( selectTab(
if (currentWebViewIndex < webViewList.size) { if (currentWebViewIndex < webViewList.size) {
currentWebViewIndex currentWebViewIndex
@ -1168,7 +1188,7 @@ abstract class CoreReaderFragment :
override fun onSpeakingStarted() { override fun onSpeakingStarted() {
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
mainMenu?.onTextToSpeechStartedTalking() mainMenu?.onTextToSpeechStartedTalking()
ttsControls?.visibility = View.VISIBLE ttsControls?.visibility = VISIBLE
setActionAndStartTTSService(ACTION_PAUSE_OR_RESUME_TTS, false) setActionAndStartTTSService(ACTION_PAUSE_OR_RESUME_TTS, false)
} }
} }
@ -1176,7 +1196,7 @@ abstract class CoreReaderFragment :
override fun onSpeakingEnded() { override fun onSpeakingEnded() {
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
mainMenu?.onTextToSpeechStoppedTalking() mainMenu?.onTextToSpeechStoppedTalking()
ttsControls?.visibility = View.GONE ttsControls?.visibility = GONE
pauseTTSButton?.setText(R.string.tts_pause) pauseTTSButton?.setText(R.string.tts_pause)
setActionAndStartTTSService(ACTION_STOP_TTS) setActionAndStartTTSService(ACTION_STOP_TTS)
} }
@ -1469,15 +1489,15 @@ abstract class CoreReaderFragment :
private fun reopenBook() { private fun reopenBook() {
hideNoBookOpenViews() hideNoBookOpenViews()
contentFrame?.visibility = View.VISIBLE contentFrame?.visibility = VISIBLE
mainMenu?.showBookSpecificMenuItems() mainMenu?.showBookSpecificMenuItems()
} }
protected fun exitBook(shouldCloseZimBook: Boolean = true) { protected fun exitBook(shouldCloseZimBook: Boolean = true) {
showNoBookOpenViews() showNoBookOpenViews()
bottomToolbar?.visibility = View.GONE bottomToolbar?.visibility = GONE
actionBar?.title = getString(R.string.reader) actionBar?.title = getString(R.string.reader)
contentFrame?.visibility = View.GONE contentFrame?.visibility = GONE
hideProgressBar() hideProgressBar()
mainMenu?.hideBookSpecificMenuItems() mainMenu?.hideBookSpecificMenuItems()
if (shouldCloseZimBook) { if (shouldCloseZimBook) {
@ -1501,7 +1521,7 @@ abstract class CoreReaderFragment :
protected fun hideProgressBar() { protected fun hideProgressBar() {
progressBar?.apply { progressBar?.apply {
visibility = View.GONE visibility = GONE
hide() hide()
} }
} }
@ -1511,7 +1531,7 @@ abstract class CoreReaderFragment :
reopenBook() reopenBook()
} }
tempWebViewForUndo?.let { tempWebViewForUndo?.let {
if (tabSwitcherRoot?.visibility == View.GONE) { if (tabSwitcherRoot?.visibility == GONE) {
// Remove the top margin from the webView when the tabSwitcher is not visible. // Remove the top margin from the webView when the tabSwitcher is not visible.
// We have added this margin in `TabsAdapter` to not show the top margin in tabs. // We have added this margin in `TabsAdapter` to not show the top margin in tabs.
// `tempWebViewForUndo` saved with that margin so before showing it to the `contentFrame` // `tempWebViewForUndo` saved with that margin so before showing it to the `contentFrame`
@ -1610,7 +1630,7 @@ abstract class CoreReaderFragment :
if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) {
ttsControls?.let { ttsControls -> ttsControls?.let { ttsControls ->
when (ttsControls.visibility) { when (ttsControls.visibility) {
View.GONE -> { GONE -> {
if (isBackToTopEnabled) { if (isBackToTopEnabled) {
backToTopButton?.hide() backToTopButton?.hide()
} }
@ -1622,7 +1642,7 @@ abstract class CoreReaderFragment :
} }
} }
View.VISIBLE -> { VISIBLE -> {
if (isBackToTopEnabled) { if (isBackToTopEnabled) {
backToTopButton?.show() backToTopButton?.show()
} }
@ -1663,14 +1683,14 @@ abstract class CoreReaderFragment :
} }
override fun onHomeMenuClicked() { override fun onHomeMenuClicked() {
if (tabSwitcherRoot?.visibility == View.VISIBLE) { if (tabSwitcherRoot?.visibility == VISIBLE) {
hideTabSwitcher() hideTabSwitcher()
} }
createNewTab() createNewTab()
} }
override fun onTabMenuClicked() { override fun onTabMenuClicked() {
if (tabSwitcherRoot?.visibility == View.VISIBLE) { if (tabSwitcherRoot?.visibility == VISIBLE) {
hideTabSwitcher() hideTabSwitcher()
selectTab(currentWebViewIndex) selectTab(currentWebViewIndex)
} else { } else {
@ -1751,9 +1771,9 @@ abstract class CoreReaderFragment :
@Suppress("MagicNumber") @Suppress("MagicNumber")
protected open fun openFullScreen() { protected open fun openFullScreen() {
(requireActivity() as CoreMainActivity).disableDrawer(false) (requireActivity() as CoreMainActivity).disableDrawer(false)
toolbarContainer?.visibility = View.GONE toolbarContainer?.visibility = GONE
bottomToolbar?.visibility = View.GONE bottomToolbar?.visibility = GONE
exitFullscreenButton?.visibility = View.VISIBLE exitFullscreenButton?.visibility = VISIBLE
exitFullscreenButton?.background?.alpha = 153 exitFullscreenButton?.background?.alpha = 153
val window = requireActivity().window val window = requireActivity().window
window.decorView.showFullScreenMode(window) window.decorView.showFullScreenMode(window)
@ -1769,9 +1789,9 @@ abstract class CoreReaderFragment :
toolbar?.let(::setUpDrawerToggle) toolbar?.let(::setUpDrawerToggle)
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
sharedPreferenceUtil?.putPrefFullScreen(false) sharedPreferenceUtil?.putPrefFullScreen(false)
toolbarContainer?.visibility = View.VISIBLE toolbarContainer?.visibility = VISIBLE
updateBottomToolbarVisibility() updateBottomToolbarVisibility()
exitFullscreenButton?.visibility = View.GONE exitFullscreenButton?.visibility = GONE
exitFullscreenButton?.background?.alpha = 255 exitFullscreenButton?.background?.alpha = 255
val window = requireActivity().window val window = requireActivity().window
window.decorView.closeFullScreenMode(window) window.decorView.closeFullScreenMode(window)
@ -1801,7 +1821,7 @@ abstract class CoreReaderFragment :
// Show content if there is `Open Library` button showing // Show content if there is `Open Library` button showing
// and we are opening the ZIM file // and we are opening the ZIM file
hideNoBookOpenViews() hideNoBookOpenViews()
contentFrame?.visibility = View.VISIBLE contentFrame?.visibility = VISIBLE
openAndSetInContainer(zimReaderSource) openAndSetInContainer(zimReaderSource)
updateTitle() updateTitle()
} else { } else {
@ -2014,13 +2034,13 @@ abstract class CoreReaderFragment :
// opens home screen when user closes all tabs // opens home screen when user closes all tabs
protected fun showNoBookOpenViews() { protected fun showNoBookOpenViews() {
noOpenBookButton?.visibility = View.VISIBLE noOpenBookButton?.visibility = VISIBLE
noOpenBookText?.visibility = View.VISIBLE noOpenBookText?.visibility = VISIBLE
} }
private fun hideNoBookOpenViews() { private fun hideNoBookOpenViews() {
noOpenBookButton?.visibility = View.GONE noOpenBookButton?.visibility = GONE
noOpenBookText?.visibility = View.GONE noOpenBookText?.visibility = GONE
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
@ -2101,7 +2121,7 @@ abstract class CoreReaderFragment :
FrameLayout.LayoutParams.WRAP_CONTENT FrameLayout.LayoutParams.WRAP_CONTENT
).apply { ).apply {
val rightAndLeftMargin = requireActivity().resources.getDimensionPixelSize( val rightAndLeftMargin = requireActivity().resources.getDimensionPixelSize(
org.kiwix.kiwixmobile.core.R.dimen.activity_horizontal_margin R.dimen.activity_horizontal_margin
) )
setMargins( setMargins(
rightAndLeftMargin, rightAndLeftMargin,
@ -2188,11 +2208,11 @@ abstract class CoreReaderFragment :
private fun updateBottomToolbarVisibility() { private fun updateBottomToolbarVisibility() {
bottomToolbar?.let { bottomToolbar?.let {
if (urlIsValid() && if (urlIsValid() &&
tabSwitcherRoot?.visibility != View.VISIBLE && !isInFullScreenMode() tabSwitcherRoot?.visibility != VISIBLE && !isInFullScreenMode()
) { ) {
it.visibility = View.VISIBLE it.visibility = VISIBLE
} else { } else {
it.visibility = View.GONE it.visibility = GONE
} }
} }
} }
@ -2332,7 +2352,7 @@ abstract class CoreReaderFragment :
} }
private fun contentUrl(articleUrl: String?): String = private fun contentUrl(articleUrl: String?): String =
"${ZimFileReader.CONTENT_PREFIX}$articleUrl".toUri().toString() "${CONTENT_PREFIX}$articleUrl".toUri().toString()
private fun redirectOrOriginal(contentUrl: String): String { private fun redirectOrOriginal(contentUrl: String): String {
zimReaderContainer?.let { zimReaderContainer?.let {
@ -2722,13 +2742,13 @@ abstract class CoreReaderFragment :
if (it > 200) { if (it > 200) {
if ( if (
(backToTopButton?.isGone == true || backToTopButton?.isInvisible == true) && (backToTopButton?.isGone == true || backToTopButton?.isInvisible == true) &&
ttsControls?.visibility == View.GONE ttsControls?.visibility == GONE
) { ) {
backToTopButton?.show() backToTopButton?.show()
} }
} else { } else {
backToTopButton?.isVisible backToTopButton?.isVisible
if (backToTopButton?.visibility == View.VISIBLE) { if (backToTopButton?.visibility == VISIBLE) {
backToTopButton?.hide() backToTopButton?.hide()
} }
} }
@ -2739,7 +2759,7 @@ abstract class CoreReaderFragment :
override fun webViewLongClick(url: String) { override fun webViewLongClick(url: String) {
var handleEvent = false var handleEvent = false
when { when {
url.startsWith(ZimFileReader.CONTENT_PREFIX) -> { url.startsWith(CONTENT_PREFIX) -> {
// This is my web site, so do not override; let my WebView load the page // This is my web site, so do not override; let my WebView load the page
handleEvent = true handleEvent = true
} }

View File

@ -0,0 +1,284 @@
/*
* 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.main.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixButton
import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost
import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem.Drawable
import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixDialogTheme
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.READER_BOTTOM_APP_BAR_LAYOUT_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TTS_BUTTONS_CONTROL_ALPHA
const val CONTENT_LOADING_PROGRESSBAR_TESTING_TAG = "contentLoadingProgressBarTestingTag"
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming")
@Composable
fun ReaderScreen(
state: ReaderScreenState,
actionMenuItems: List<ActionMenuItem>,
navigationIcon: @Composable () -> Unit
) {
KiwixDialogTheme {
Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = { KiwixAppBar(R.string.note, navigationIcon, actionMenuItems) },
floatingActionButton = { BackToTopFab(state) }
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
ShowProgressBarIfZIMFilePageIsLoading(state)
if (state.isNoBookOpenInReader) {
NoBookOpenView(state.onOpenLibraryButtonClicked)
}
}
TtsControls(state)
ShowFullScreenView(state)
ShowDonationLayout(state)
}
}
}
}
@Composable
private fun ShowFullScreenView(state: ReaderScreenState) {
if (state.fullScreenItem.first) {
state.fullScreenItem.second
}
}
@Composable
private fun ShowProgressBarIfZIMFilePageIsLoading(state: ReaderScreenState) {
if (state.pageLoadingItem.first) {
ContentLoadingProgressBar(
modifier = Modifier.testTag(CONTENT_LOADING_PROGRESSBAR_TESTING_TAG),
progressBarStyle = ProgressBarStyle.HORIZONTAL,
progress = state.pageLoadingItem.second
)
}
}
@Composable
private fun NoBookOpenView(
onOpenLibraryButtonClicked: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = FOUR_DP)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.no_open_book),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Medium),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(EIGHT_DP))
KiwixButton(
buttonText = stringResource(R.string.open_library),
clickListener = onOpenLibraryButtonClicked
)
}
}
@Composable
private fun BoxScope.TtsControls(state: ReaderScreenState) {
if (state.showTtsControls) {
Row(modifier = Modifier.align(Alignment.TopCenter)) {
Button(
onClick = state.onPauseTtsClick,
modifier = Modifier
.weight(1f)
.alpha(TTS_BUTTONS_CONTROL_ALPHA)
) {
Text(
text = stringResource(R.string.tts_pause),
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.width(FOUR_DP))
Button(
onClick = state.onStopTtsClick,
modifier = Modifier
.weight(1f)
.alpha(TTS_BUTTONS_CONTROL_ALPHA)
) {
Text(
text = stringResource(R.string.stop),
fontWeight = FontWeight.Bold
)
}
}
}
}
@Composable
private fun BackToTopFab(state: ReaderScreenState) {
if (state.showBackToTopButton) {
FloatingActionButton(
onClick = state.backToTopButtonClick,
modifier = Modifier,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
shape = FloatingActionButtonDefaults.smallShape
) {
Icon(
painter = Drawable(R.drawable.action_find_previous).toPainter(),
contentDescription = stringResource(R.string.pref_back_to_top),
tint = White
)
}
}
}
@Composable
private fun BottomAppBarOfReaderScreen(
onBookmarkClick: () -> Unit,
onBackClick: () -> Unit,
onHomeClick: () -> Unit,
onForwardClick: () -> Unit,
onTocClick: () -> Unit
) {
BottomAppBar(
containerColor = Black,
contentColor = White
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(READER_BOTTOM_APP_BAR_LAYOUT_HEIGHT),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Bookmark Icon
BottomAppBarButtonIcon(
onBookmarkClick,
Drawable(R.drawable.ic_bookmark_border_24dp),
stringResource(R.string.bookmarks)
)
// Back Icon(for going to previous page)
BottomAppBarButtonIcon(
onBackClick,
Drawable(R.drawable.ic_keyboard_arrow_left_24dp),
stringResource(R.string.go_to_previous_page)
)
// Home Icon(to open the home page of ZIM file)
BottomAppBarButtonIcon(
onHomeClick,
Drawable(R.drawable.action_home),
stringResource(R.string.menu_home)
)
// Forward Icon(for going to next page)
BottomAppBarButtonIcon(
onForwardClick,
Drawable(R.drawable.ic_keyboard_arrow_right_24dp),
stringResource(R.string.go_to_next_page)
)
// Toggle Icon(to open the table of content in right side bar)
BottomAppBarButtonIcon(
onTocClick,
Drawable(R.drawable.ic_toc_24dp),
stringResource(R.string.table_of_contents)
)
}
}
}
@Composable
private fun BottomAppBarButtonIcon(
onClick: () -> Unit,
buttonIcon: IconItem,
contentDescription: String
) {
IconButton(onClick = onClick) {
Icon(
buttonIcon.toPainter(),
contentDescription,
modifier = Modifier.size(READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE)
)
}
}
@Composable
private fun BoxScope.ShowDonationLayout(state: ReaderScreenState) {
if (state.shouldShowDonationPopup) {
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
) {
// TODO create donation popup layout.
}
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.main.reader
import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.platform.ComposeView
/**
* Represents the UI state for the Reader Screen.
*
* This data class encapsulates all UI-related states in a single object,
* reducing complexity in the Fragment.
*/
data class ReaderScreenState(
/**
* Handles snack bar messages and displays.
*/
val snackBarHostState: SnackbarHostState,
/**
* Manages the showing of "No open book" message and button.
*/
val isNoBookOpenInReader: Boolean,
/**
* Handles when open library button clicks.
*/
val onOpenLibraryButtonClicked: () -> Unit,
/**
* Manages the showing of "ProgressBar" when ZIM file page is loading.
*
* A [Pair] containing:
* - [Boolean]: Whether page is loading.
* - [Int]: progress of page loading.
*/
val pageLoadingItem: Pair<Boolean, Int>,
/**
* Manages the showing of "Donation" layout.
*/
val shouldShowDonationPopup: Boolean,
/**
* Manages the showing of "Full screen view".
*
* A [Pair] containing:
* - [Boolean]: Whether to show/hide full screen mode.
* - [ComposeView]: full screen view.
*/
val fullScreenItem: Pair<Boolean, ComposeView>,
/**
* Manages the showing of "BackToTop" fab button.
*/
val showBackToTopButton: Boolean,
/**
* Handles the click of "BackToTop" fab button.
*/
val backToTopButtonClick: () -> Unit,
val showFullscreenButton: Boolean = false,
val onExitFullscreenClick: () -> Unit = {},
val showTtsControls: Boolean = false,
val onPauseTtsClick: () -> Unit = {},
val onStopTtsClick: () -> Unit = {},
)

View File

@ -25,7 +25,7 @@ import kotlinx.parcelize.Parcelize
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationResultOnCurrent import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationResultOnCurrent
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.SEARCH_ITEM_TITLE_KEY import org.kiwix.kiwixmobile.core.main.reader.SEARCH_ITEM_TITLE_KEY
import org.kiwix.kiwixmobile.core.reader.addContentPrefix import org.kiwix.kiwixmobile.core.reader.addContentPrefix
import org.kiwix.kiwixmobile.core.search.SearchListItem import org.kiwix.kiwixmobile.core.search.SearchListItem
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED

View File

@ -179,5 +179,9 @@ object ComposeDimens {
// Settings screen dimes // Settings screen dimes
val STORAGE_LOADING_PROGRESS_BAR_SIZE = 40.dp val STORAGE_LOADING_PROGRESS_BAR_SIZE = 40.dp
val CATEGORY_TITLE_TEXT_SIZE = 14.sp val CATEGORY_TITLE_TEXT_SIZE = 14.sp
val PREFERENCE_TITLE_TEXT_SIZE = 18.sp
// Reader screen dimes
val READER_BOTTOM_APP_BAR_LAYOUT_HEIGHT = 48.dp
val READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE = 30.dp
const val TTS_BUTTONS_CONTROL_ALPHA = 0.6f
} }

View File

@ -37,9 +37,9 @@ import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.extensions.browserIntent import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.getResizedDrawable import org.kiwix.kiwixmobile.core.extensions.getResizedDrawable
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.main.CoreReaderFragment import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.MainMenu import org.kiwix.kiwixmobile.core.main.MainMenu
import org.kiwix.kiwixmobile.core.main.RestoreOrigin import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.utils.LanguageUtils import org.kiwix.kiwixmobile.core.utils.LanguageUtils