Refactored all functionalities of the Reader's menu: showing or hiding menu items based on business logic, updating the tab item count, and more.

* Improved the KiwixAppBar to support custom views in menu items — enabling custom UI like the tab switcher.
* Fixed: TTS controls were not displaying correctly on the UI.
* Refactored the TTS functionality to align with the Compose UI architecture.
* Fixed: Some lint issues and improve the code quality.
This commit is contained in:
MohitMaliFtechiz 2025-06-18 02:23:32 +05:30
parent 5c879484a6
commit f654a64b7e
9 changed files with 359 additions and 160 deletions

View File

@ -197,7 +197,7 @@ class KiwixReaderFragment : CoreReaderFragment() {
progressBar?.progress = 0
contentFrame?.visibility = View.VISIBLE
}
mainMenu?.showWebViewOptions(true)
readerMenuState?.showWebViewOptions(true)
if (webViewList.isEmpty()) {
exitBook(shouldCloseZimBook)
} else {
@ -231,7 +231,7 @@ class KiwixReaderFragment : CoreReaderFragment() {
override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateOptionsMenu(menu, menuInflater)
if (zimReaderContainer?.zimFileReader == null) {
mainMenu?.hideBookSpecificMenuItems()
readerMenuState?.hideBookSpecificMenuItems()
}
}

View File

@ -13,7 +13,7 @@
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID>
<ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID>
<ID>LongParameterList:Repository.kt$Repository$( private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup, videoView: ViewGroup, webViewClient: CoreWebViewClient, private val toolbarView: View, private val bottomBarView: View, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup?, videoView: ViewGroup?, webViewClient: CoreWebViewClient, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>
<ID>MagicNumber:DownloadItem.kt$DownloadItem$1000L</ID>
@ -23,6 +23,7 @@
<ID>MagicNumber:JNIInitialiser.kt$JNIInitialiser$1024</ID>
<ID>MagicNumber:Byte.kt$Byte$1024.0</ID>
<ID>MagicNumber:MainMenu.kt$MainMenu$99</ID>
<ID>MagicNumber:ReaderMenuState.kt$ReaderMenuState$99</ID>
<ID>MagicNumber:OnSwipeTouchListener.kt$OnSwipeTouchListener.GestureListener$100</ID>
<ID>MagicNumber:SearchResultGenerator.kt$ZimSearchResultGenerator$200</ID>
<ID>MagicNumber:Seconds.kt$Seconds$24</ID>

View File

@ -74,6 +74,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.platform.ComposeView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Group
@ -106,6 +107,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@ -368,6 +370,7 @@ abstract class CoreReaderFragment :
private var isReadAloudServiceRunning = false
private var libkiwixBook: Book? = null
protected var readerMenuState: ReaderMenuState? = null
private var composeView: ComposeView? = null
protected val readerScreenState = mutableStateOf(
ReaderScreenState(
@ -384,7 +387,7 @@ abstract class CoreReaderFragment :
onExitFullscreenClick = { closeFullScreen() },
showTtsControls = false,
onPauseTtsClick = { pauseTts() },
pauseTtsButtonText = "",
pauseTtsButtonText = context?.getString(R.string.tts_pause).orEmpty(),
onStopTtsClick = { stopTts() },
kiwixWebViewList = webViewList,
bookmarkButtonItem = Triple(
@ -497,6 +500,7 @@ abstract class CoreReaderFragment :
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
readerMenuState = createMainMenu()
composeView?.apply {
setContent {
val lazyListState = rememberLazyListState()
@ -504,6 +508,13 @@ abstract class CoreReaderFragment :
LaunchedEffect(isBottomNavVisible) {
(requireActivity() as CoreMainActivity).toggleBottomNavigation(isBottomNavVisible)
}
LaunchedEffect(Unit) {
snapshotFlow { webViewList.size }
.distinctUntilChanged()
.collect { size ->
updateTabIcon(size)
}
}
LaunchedEffect(Unit) {
readerScreenState.update {
copy(
@ -514,7 +525,7 @@ abstract class CoreReaderFragment :
}
ReaderScreen(
state = readerScreenState.value,
actionMenuItems = emptyList(),
actionMenuItems = readerMenuState?.menuItems.orEmpty(),
navigationIcon = {
NavigationIcon(
iconItem = IconItem.Vector(Icons.Filled.Menu),
@ -806,7 +817,7 @@ abstract class CoreReaderFragment :
}
private val isInTabSwitcher: Boolean
get() = mainMenu?.isInTabSwitcher() == true
get() = readerMenuState?.isInTabSwitcher == true
private fun setupDocumentParser() {
documentParser = DocumentParser(object : SectionsListener {
@ -865,7 +876,7 @@ abstract class CoreReaderFragment :
).apply {
registerAdapterDataObserver(object : AdapterDataObserver() {
override fun onChanged() {
mainMenu?.updateTabIcon(itemCount)
readerMenuState?.updateTabIcon(itemCount)
}
})
}
@ -948,7 +959,7 @@ abstract class CoreReaderFragment :
// reflected correctly.
tabsAdapter.notifyDataSetChanged()
}
mainMenu?.showTabSwitcherOptions()
readerMenuState?.showTabSwitcherOptions()
}
/**
@ -1019,7 +1030,7 @@ abstract class CoreReaderFragment :
}
progressBar?.hide()
selectTab(currentWebViewIndex)
mainMenu?.showWebViewOptions(urlIsValid())
readerMenuState?.showWebViewOptions(urlIsValid())
// Reset the top margin of web views to 0 to remove any previously set margin
// This ensures that the web views are displayed without any additional top margin for kiwix custom apps.
setTopMarginToWebViews(0)
@ -1267,17 +1278,21 @@ abstract class CoreReaderFragment :
object : OnSpeakingListener {
override fun onSpeakingStarted() {
requireActivity().runOnUiThread {
mainMenu?.onTextToSpeechStartedTalking()
ttsControls?.visibility = VISIBLE
readerMenuState?.onTextToSpeechStarted()
readerScreenState.update { copy(showTtsControls = true) }
setActionAndStartTTSService(ACTION_PAUSE_OR_RESUME_TTS, false)
}
}
override fun onSpeakingEnded() {
requireActivity().runOnUiThread {
mainMenu?.onTextToSpeechStoppedTalking()
ttsControls?.visibility = GONE
pauseTTSButton?.setText(R.string.tts_pause)
readerMenuState?.onTextToSpeechStopped()
readerScreenState.update {
copy(
showTtsControls = false,
pauseTtsButtonText = context?.getString(R.string.tts_pause).orEmpty()
)
}
setActionAndStartTTSService(ACTION_STOP_TTS)
}
}
@ -1293,12 +1308,16 @@ abstract class CoreReaderFragment :
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> {
if (tts?.currentTTSTask?.paused == false) tts?.pauseOrResume()
pauseTTSButton?.setText(R.string.tts_resume)
readerScreenState.update {
copy(pauseTtsButtonText = context?.getString(R.string.tts_resume).orEmpty())
}
setActionAndStartTTSService(ACTION_PAUSE_OR_RESUME_TTS, true)
}
AudioManager.AUDIOFOCUS_GAIN -> {
pauseTTSButton?.setText(R.string.tts_pause)
readerScreenState.update {
copy(pauseTtsButtonText = context?.getString(R.string.tts_pause).orEmpty())
}
setActionAndStartTTSService(ACTION_PAUSE_OR_RESUME_TTS, false)
}
}
@ -1543,6 +1562,10 @@ abstract class CoreReaderFragment :
return webView
}
private fun updateTabIcon(size: Int) {
readerMenuState?.updateTabIcon(size)
}
private fun closeTab(index: Int) {
if (currentTtsWebViewIndex == index) {
onReadAloudStop()
@ -1579,7 +1602,7 @@ abstract class CoreReaderFragment :
private fun reopenBook() {
hideNoBookOpenViews()
contentFrame?.visibility = VISIBLE
mainMenu?.showBookSpecificMenuItems()
readerMenuState?.showBookSpecificMenuItems()
}
protected fun exitBook(shouldCloseZimBook: Boolean = true) {
@ -1592,7 +1615,7 @@ abstract class CoreReaderFragment :
}
contentFrame?.visibility = GONE
hideProgressBar()
mainMenu?.hideBookSpecificMenuItems()
readerMenuState?.hideBookSpecificMenuItems()
if (shouldCloseZimBook) {
closeZimBook()
}
@ -1702,11 +1725,20 @@ abstract class CoreReaderFragment :
@Suppress("NestedBlockDepth")
override fun onReadAloudMenuClicked() {
if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) {
ttsControls?.let { ttsControls ->
when (ttsControls.visibility) {
GONE -> {
if (readerScreenState.value.showTtsControls) {
// currently TTS is running
if (isBackToTopEnabled) {
backToTopButton?.hide()
readerScreenState.update { copy(showBackToTopButton = true) }
backToTopButton?.show()
}
tts?.stop()
} else {
// TTS is not running.
if (isBackToTopEnabled) {
readerScreenState.update { copy(showBackToTopButton = false) }
}
readerScreenState.update {
copy(pauseTtsButtonText = context?.getString(R.string.tts_pause).orEmpty())
}
if (tts?.isInitialized == false) {
isReadSelection = false
@ -1715,17 +1747,6 @@ abstract class CoreReaderFragment :
startReadAloud()
}
}
VISIBLE -> {
if (isBackToTopEnabled) {
backToTopButton?.show()
}
tts?.stop()
}
else -> {}
}
}
} else {
requestNotificationPermission()
}
@ -1945,7 +1966,7 @@ abstract class CoreReaderFragment :
if (!isFromManageExternalLaunch) {
openArticle(UNINITIALISER_ADDRESS)
}
mainMenu?.onFileOpened(urlIsValid())
readerMenuState?.onFileOpened(urlIsValid())
setUpBookmarks(zimFileReader)
} ?: kotlin.run {
// If the ZIM file is not opened properly (especially for ZIM chunks), exit the book to
@ -2558,9 +2579,10 @@ abstract class CoreReaderFragment :
* WARNING: If modifying this method, ensure thorough testing with custom apps
* to verify proper functionality.
*/
protected open fun createMainMenu(menu: Menu?): ReaderMenuState? =
protected open fun createMainMenu(): ReaderMenuState =
ReaderMenuState(
this,
isUrlValidInitially = urlIsValid(),
disableReadAloud = false,
disableTabs = false,
disableSearch = false
@ -2799,6 +2821,7 @@ abstract class CoreReaderFragment :
}
override fun webViewTitleUpdated(title: String) {
updateTabIcon(webViewList.size)
tabsAdapter?.notifyDataSetChanged()
}

View File

@ -18,16 +18,39 @@
package org.kiwix.kiwixmobile.core.main.reader
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.MATERIAL_MINIMUM_HEIGHT_AND_WIDTH
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIX_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TAB_SWITCHER_CORNER_RADIUS
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TAB_SWITCHER_TEXT_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWELVE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWENTY_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
const val READ_ALOUD_MENU_ITEM_TESTING_TAG = "readAloudMenuItemTestingTag"
const val TAKE_NOTE_MENU_ITEM_TESTING_TAG = "takeNoteMenuItemTestingTag"
@ -38,6 +61,7 @@ const val TAB_MENU_ITEM_TESTING_TAG = "tabMenuItemTestingTag"
@Stable
class ReaderMenuState(
private val menuClickListener: MenuClickListener,
private val isUrlValidInitially: Boolean,
private val disableReadAloud: Boolean = false,
private val disableTabs: Boolean = false,
private val disableSearch: Boolean = false
@ -52,50 +76,105 @@ class ReaderMenuState(
fun onSearchMenuClickedMenuClicked()
}
val menuItems = mutableStateListOf<ActionMenuItem>()
private val menuItemVisibility = mutableMapOf<MenuItemType, Boolean>().apply {
put(MenuItemType.Search, true)
put(MenuItemType.TabSwitcher, true)
put(MenuItemType.AddNote, true)
put(MenuItemType.RandomArticle, true)
put(MenuItemType.Fullscreen, true)
put(MenuItemType.ReadAloud, true)
}
var isInTabSwitcher by mutableStateOf(false)
private set
var isReadingAloud by mutableStateOf(false)
private set
private var isReadingAloud by mutableStateOf(false)
var webViewCount by mutableStateOf(0)
var urlIsValid by mutableStateOf(false)
var zimFileReaderAvailable by mutableStateOf(false)
private var webViewCount by mutableStateOf(0)
private var urlIsValid by mutableStateOf(false)
fun onTabsChanged(count: Int) {
fun updateTabIcon(count: Int) {
webViewCount = count
updateMenuItems()
}
fun onUrlValidityChanged(valid: Boolean) {
init {
showWebViewOptions(isUrlValidInitially)
}
fun showWebViewOptions(valid: Boolean) {
isInTabSwitcher = false
urlIsValid = valid
setVisibility(
urlIsValid,
MenuItemType.RandomArticle,
MenuItemType.Search,
MenuItemType.ReadAloud,
MenuItemType.Fullscreen,
MenuItemType.AddNote,
MenuItemType.TabSwitcher
)
}
fun onZimFileReaderAvailable(available: Boolean) {
zimFileReaderAvailable = available
fun onFileOpened(urlIsValid: Boolean) {
showWebViewOptions(urlIsValid)
}
fun onTextToSpeechStarted() {
isReadingAloud = true
updateMenuItems()
}
fun onTextToSpeechStopped() {
isReadingAloud = false
updateMenuItems()
}
fun exitTabSwitcher() {
isInTabSwitcher = false
fun hideBookSpecificMenuItems() {
setVisibility(
false,
MenuItemType.Search,
MenuItemType.TabSwitcher,
MenuItemType.RandomArticle,
MenuItemType.AddNote,
MenuItemType.ReadAloud
)
}
@Suppress("LongMethod", "MagicNumber")
fun getActionMenuItems(): List<ActionMenuItem> {
if (isInTabSwitcher) {
return emptyList()
fun showBookSpecificMenuItems() {
setVisibility(
true,
MenuItemType.Search,
MenuItemType.TabSwitcher,
MenuItemType.RandomArticle,
MenuItemType.AddNote,
MenuItemType.ReadAloud
)
}
val list = mutableListOf<ActionMenuItem>()
fun showTabSwitcherOptions() {
isInTabSwitcher = true
setVisibility(
false,
MenuItemType.RandomArticle,
MenuItemType.ReadAloud,
MenuItemType.AddNote,
MenuItemType.Fullscreen
)
}
if (!disableSearch && urlIsValid) {
list += ActionMenuItem(
private fun updateMenuItems() {
menuItems.clear()
addSearchMenuItem()
addTabMenuItem()
addReaderMenuItems()
}
private fun addSearchMenuItem() {
if (menuItemVisibility[MenuItemType.Search] == true && !disableSearch && urlIsValid) {
menuItems += ActionMenuItem(
icon = IconItem.Drawable(R.drawable.action_search),
contentDescription = R.string.search_label,
onClick = { menuClickListener.onSearchMenuClickedMenuClicked() },
@ -103,11 +182,13 @@ class ReaderMenuState(
testingTag = SEARCH_ICON_TESTING_TAG
)
}
}
if (!disableTabs) {
private fun addTabMenuItem() {
if (!disableTabs && urlIsValid) {
val tabLabel = if (webViewCount > 99) ":D" else "$webViewCount"
list += ActionMenuItem(
icon = IconItem.Vector(Icons.Default.Add),
menuItems += ActionMenuItem(
icon = null,
contentDescription = R.string.switch_tabs,
onClick = {
isInTabSwitcher = true
@ -115,42 +196,102 @@ class ReaderMenuState(
},
isInOverflow = false,
iconButtonText = tabLabel,
testingTag = TAB_MENU_ITEM_TESTING_TAG
testingTag = TAB_MENU_ITEM_TESTING_TAG,
customView = { TabSwitcherBadge(tabLabel = tabLabel) }
)
}
}
if (urlIsValid) {
list += listOf(
ActionMenuItem(
@Composable
fun TabSwitcherBadge(tabLabel: String, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(MATERIAL_MINIMUM_HEIGHT_AND_WIDTH)
.padding(TWELVE_DP),
contentAlignment = Alignment.Center
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(TAB_SWITCHER_CORNER_RADIUS))
.background(Black)
.border(ONE_DP, White, RoundedCornerShape(TAB_SWITCHER_CORNER_RADIUS))
.padding(horizontal = SIX_DP, vertical = TWO_DP)
.defaultMinSize(minWidth = TWENTY_DP, minHeight = TWENTY_DP),
contentAlignment = Alignment.Center
) {
Text(
text = tabLabel,
color = White,
fontWeight = FontWeight.Bold,
fontSize = TAB_SWITCHER_TEXT_SIZE,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
private fun addReaderMenuItems() {
if (!urlIsValid) return
if (menuItemVisibility[MenuItemType.Search] == true) {
menuItems += ActionMenuItem(
icon = IconItem.Drawable(R.drawable.ic_add_note),
contentDescription = R.string.take_notes,
onClick = { menuClickListener.onAddNoteMenuClicked() },
testingTag = TAKE_NOTE_MENU_ITEM_TESTING_TAG
),
ActionMenuItem(
testingTag = TAKE_NOTE_MENU_ITEM_TESTING_TAG,
isInOverflow = true
)
}
if (menuItemVisibility[MenuItemType.RandomArticle] == true) {
menuItems += ActionMenuItem(
contentDescription = R.string.menu_random_article,
onClick = { menuClickListener.onRandomArticleMenuClicked() },
testingTag = RANDOM_ARTICLE_MENU_ITEM_TESTING_TAG
),
ActionMenuItem(
testingTag = RANDOM_ARTICLE_MENU_ITEM_TESTING_TAG,
isInOverflow = true
)
}
if (menuItemVisibility[MenuItemType.Fullscreen] == true) {
menuItems += ActionMenuItem(
contentDescription = R.string.menu_full_screen,
onClick = { menuClickListener.onFullscreenMenuClicked() },
testingTag = FULL_SCREEN_MENU_ITEM_TESTING_TAG
)
testingTag = FULL_SCREEN_MENU_ITEM_TESTING_TAG,
isInOverflow = true
)
}
if (!disableReadAloud) {
list += ActionMenuItem(
if (menuItemVisibility[MenuItemType.ReadAloud] == true && !disableReadAloud) {
menuItems += ActionMenuItem(
contentDescription = if (isReadingAloud) R.string.menu_read_aloud_stop else R.string.menu_read_aloud,
onClick = {
isReadingAloud = !isReadingAloud
menuClickListener.onReadAloudMenuClicked()
},
testingTag = READ_ALOUD_MENU_ITEM_TESTING_TAG
testingTag = READ_ALOUD_MENU_ITEM_TESTING_TAG,
isInOverflow = true
)
}
}
return list
private fun setVisibility(visible: Boolean, vararg types: MenuItemType) {
types.forEach {
if (it == MenuItemType.Search && disableSearch) {
menuItemVisibility[it] = false
} else {
menuItemVisibility[it] = visible
}
}
updateMenuItems()
}
}
enum class MenuItemType {
Search,
TabSwitcher,
AddNote,
RandomArticle,
Fullscreen,
ReadAloud
}

View File

@ -150,6 +150,9 @@ fun ReaderScreen(
} else {
ShowZIMFileContent(state)
ShowProgressBarIfZIMFilePageIsLoading(state)
Column(
modifier = Modifier.align(Alignment.BottomCenter)
) {
TtsControls(state)
BottomAppBarOfReaderScreen(
state.bookmarkButtonItem,
@ -159,6 +162,7 @@ fun ReaderScreen(
state.onTocClick,
state.shouldShowBottomAppBar
)
}
ShowFullScreenView(state)
}
ShowDonationLayout(state)
@ -225,9 +229,9 @@ private fun NoBookOpenView(
}
@Composable
private fun BoxScope.TtsControls(state: ReaderScreenState) {
private fun TtsControls(state: ReaderScreenState) {
if (state.showTtsControls) {
Row(modifier = Modifier.align(Alignment.BottomCenter)) {
Row {
Button(
onClick = state.onPauseTtsClick,
modifier = Modifier
@ -235,7 +239,7 @@ private fun BoxScope.TtsControls(state: ReaderScreenState) {
.alpha(TTS_BUTTONS_CONTROL_ALPHA)
) {
Text(
text = state.pauseTtsButtonText,
text = state.pauseTtsButtonText.uppercase(),
fontWeight = FontWeight.Bold
)
}
@ -247,7 +251,7 @@ private fun BoxScope.TtsControls(state: ReaderScreenState) {
.alpha(TTS_BUTTONS_CONTROL_ALPHA)
) {
Text(
text = stringResource(R.string.stop),
text = stringResource(R.string.stop).uppercase(),
fontWeight = FontWeight.Bold
)
}
@ -276,7 +280,7 @@ private fun BackToTopFab(state: ReaderScreenState) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BoxScope.BottomAppBarOfReaderScreen(
private fun BottomAppBarOfReaderScreen(
bookmarkButtonItem: Triple<() -> Unit, () -> Unit, Drawable>,
previousPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
onHomeButtonClick: () -> Unit,
@ -288,7 +292,6 @@ private fun BoxScope.BottomAppBarOfReaderScreen(
BottomAppBar(
containerColor = Black,
contentColor = White,
modifier = Modifier.align(Alignment.BottomCenter),
scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
) {
Row(

View File

@ -18,8 +18,10 @@
package org.kiwix.kiwixmobile.core.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
@ -135,44 +137,13 @@ private fun AppBarTitle(
)
}
@Suppress("LongMethod")
@Composable
private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
var overflowExpanded by remember { mutableStateOf(false) }
Row {
val (mainActions, overflowActions) = actionMenuItems.partition { !it.isInOverflow }
mainActions.forEach { menuItem ->
val modifier = menuItem.modifier.testTag(menuItem.testingTag)
// If icon is not null show the icon.
menuItem.icon?.let {
IconButton(
enabled = menuItem.isEnabled,
onClick = menuItem.onClick,
modifier = modifier
) {
Icon(
painter = it.toPainter(),
contentDescription = stringResource(menuItem.contentDescription),
tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray
)
}
} ?: run {
// Else show the textView button in menuItem.
TextButton(
enabled = menuItem.isEnabled,
onClick = menuItem.onClick,
modifier = modifier
) {
Text(
text = menuItem.iconButtonText.uppercase(),
color = if (menuItem.isEnabled) Color.White else Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
MainMenuItems(mainActions)
if (overflowActions.isNotEmpty()) {
IconButton(onClick = { overflowExpanded = true }) {
Icon(
@ -182,17 +153,73 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
)
}
}
OverflowMenuItems(overflowExpanded, overflowActions) { overflowExpanded = false }
}
}
@Composable
private fun MainMenuItems(mainActions: List<ActionMenuItem>) {
mainActions.forEach { menuItem ->
val modifier = menuItem.modifier.testTag(menuItem.testingTag)
menuItem.customView?.let { customComposable ->
Box(modifier = modifier.clickable(enabled = menuItem.isEnabled) { menuItem.onClick() }) {
customComposable()
}
} ?: run {
menuItem.icon?.let { iconItem ->
IconButton(
enabled = menuItem.isEnabled,
onClick = menuItem.onClick,
modifier = modifier
) {
Icon(
painter = iconItem.toPainter(),
contentDescription = stringResource(menuItem.contentDescription),
tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray
)
}
} ?: run {
TextButton(
enabled = menuItem.isEnabled,
onClick = menuItem.onClick,
modifier = modifier
) {
Text(
text = menuItem.iconButtonText.uppercase(),
color = if (menuItem.isEnabled) Color.White else Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
@Composable
private fun OverflowMenuItems(
overflowExpanded: Boolean,
overflowActions: List<ActionMenuItem>,
onDismiss: () -> Unit
) {
DropdownMenu(
expanded = overflowExpanded,
onDismissRequest = { overflowExpanded = false }
onDismissRequest = onDismiss
) {
overflowActions.forEach { menuItem ->
overflowActions.forEachIndexed { index, menuItem ->
DropdownMenuItem(
text = {
Text(text = menuItem.iconButtonText)
Column {
Text(
text = menuItem.iconButtonText.ifEmpty {
stringResource(id = menuItem.contentDescription)
}
)
}
},
onClick = {
overflowExpanded = false
onDismiss()
menuItem.onClick()
},
enabled = menuItem.isEnabled
@ -200,7 +227,6 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
}
}
}
}
@Composable
fun rememberBottomNavigationVisibility(lazyListState: LazyListState?): Boolean {

View File

@ -19,6 +19,7 @@
package org.kiwix.kiwixmobile.core.ui.models
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.kiwix.kiwixmobile.core.ui.theme.White
@ -32,5 +33,6 @@ data class ActionMenuItem(
val iconButtonText: String = "",
val testingTag: String,
val modifier: Modifier = Modifier,
val isInOverflow: Boolean = false
val isInOverflow: Boolean = false,
val customView: (@Composable () -> Unit)? = null
)

View File

@ -185,4 +185,6 @@ object ComposeDimens {
val READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE = 30.dp
const val TTS_BUTTONS_CONTROL_ALPHA = 0.6f
val CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING = 24.dp
val TAB_SWITCHER_TEXT_SIZE = 12.sp
const val TAB_SWITCHER_CORNER_RADIUS = 10
}

View File

@ -308,9 +308,10 @@ class CustomReaderFragment : CoreReaderFragment() {
* provided configuration. It takes into account whether read aloud and tabs are enabled or disabled
* and creates the menu accordingly.
*/
override fun createMainMenu(menu: Menu?): ReaderMenuState? =
override fun createMainMenu(): ReaderMenuState =
ReaderMenuState(
this,
isUrlValidInitially = urlIsValid(),
disableReadAloud = BuildConfig.DISABLE_READ_ALOUD,
disableTabs = BuildConfig.DISABLE_TABS,
disableSearch = BuildConfig.DISABLE_TITLE