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

View File

@ -18,16 +18,39 @@
package org.kiwix.kiwixmobile.core.main.reader package org.kiwix.kiwixmobile.core.main.reader
import androidx.compose.material.icons.Icons import androidx.compose.foundation.background
import androidx.compose.material.icons.filled.Add 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.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue 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.R
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem import org.kiwix.kiwixmobile.core.ui.models.IconItem
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 READ_ALOUD_MENU_ITEM_TESTING_TAG = "readAloudMenuItemTestingTag"
const val TAKE_NOTE_MENU_ITEM_TESTING_TAG = "takeNoteMenuItemTestingTag" const val TAKE_NOTE_MENU_ITEM_TESTING_TAG = "takeNoteMenuItemTestingTag"
@ -38,6 +61,7 @@ const val TAB_MENU_ITEM_TESTING_TAG = "tabMenuItemTestingTag"
@Stable @Stable
class ReaderMenuState( class ReaderMenuState(
private val menuClickListener: MenuClickListener, private val menuClickListener: MenuClickListener,
private val isUrlValidInitially: Boolean,
private val disableReadAloud: Boolean = false, private val disableReadAloud: Boolean = false,
private val disableTabs: Boolean = false, private val disableTabs: Boolean = false,
private val disableSearch: Boolean = false private val disableSearch: Boolean = false
@ -52,50 +76,105 @@ class ReaderMenuState(
fun onSearchMenuClickedMenuClicked() 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) var isInTabSwitcher by mutableStateOf(false)
private set private set
var isReadingAloud by mutableStateOf(false) private var isReadingAloud by mutableStateOf(false)
private set
var webViewCount by mutableStateOf(0) private var webViewCount by mutableStateOf(0)
var urlIsValid by mutableStateOf(false) private var urlIsValid by mutableStateOf(false)
var zimFileReaderAvailable by mutableStateOf(false)
fun onTabsChanged(count: Int) { fun updateTabIcon(count: Int) {
webViewCount = count webViewCount = count
updateMenuItems()
} }
fun onUrlValidityChanged(valid: Boolean) { init {
showWebViewOptions(isUrlValidInitially)
}
fun showWebViewOptions(valid: Boolean) {
isInTabSwitcher = false
urlIsValid = valid urlIsValid = valid
setVisibility(
urlIsValid,
MenuItemType.RandomArticle,
MenuItemType.Search,
MenuItemType.ReadAloud,
MenuItemType.Fullscreen,
MenuItemType.AddNote,
MenuItemType.TabSwitcher
)
} }
fun onZimFileReaderAvailable(available: Boolean) { fun onFileOpened(urlIsValid: Boolean) {
zimFileReaderAvailable = available showWebViewOptions(urlIsValid)
} }
fun onTextToSpeechStarted() { fun onTextToSpeechStarted() {
isReadingAloud = true isReadingAloud = true
updateMenuItems()
} }
fun onTextToSpeechStopped() { fun onTextToSpeechStopped() {
isReadingAloud = false isReadingAloud = false
updateMenuItems()
} }
fun exitTabSwitcher() { fun hideBookSpecificMenuItems() {
isInTabSwitcher = false setVisibility(
false,
MenuItemType.Search,
MenuItemType.TabSwitcher,
MenuItemType.RandomArticle,
MenuItemType.AddNote,
MenuItemType.ReadAloud
)
} }
@Suppress("LongMethod", "MagicNumber") fun showBookSpecificMenuItems() {
fun getActionMenuItems(): List<ActionMenuItem> { setVisibility(
if (isInTabSwitcher) { true,
return emptyList() 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) { private fun updateMenuItems() {
list += ActionMenuItem( menuItems.clear()
addSearchMenuItem()
addTabMenuItem()
addReaderMenuItems()
}
private fun addSearchMenuItem() {
if (menuItemVisibility[MenuItemType.Search] == true && !disableSearch && urlIsValid) {
menuItems += ActionMenuItem(
icon = IconItem.Drawable(R.drawable.action_search), icon = IconItem.Drawable(R.drawable.action_search),
contentDescription = R.string.search_label, contentDescription = R.string.search_label,
onClick = { menuClickListener.onSearchMenuClickedMenuClicked() }, onClick = { menuClickListener.onSearchMenuClickedMenuClicked() },
@ -103,11 +182,13 @@ class ReaderMenuState(
testingTag = SEARCH_ICON_TESTING_TAG testingTag = SEARCH_ICON_TESTING_TAG
) )
} }
}
if (!disableTabs) { private fun addTabMenuItem() {
if (!disableTabs && urlIsValid) {
val tabLabel = if (webViewCount > 99) ":D" else "$webViewCount" val tabLabel = if (webViewCount > 99) ":D" else "$webViewCount"
list += ActionMenuItem( menuItems += ActionMenuItem(
icon = IconItem.Vector(Icons.Default.Add), icon = null,
contentDescription = R.string.switch_tabs, contentDescription = R.string.switch_tabs,
onClick = { onClick = {
isInTabSwitcher = true isInTabSwitcher = true
@ -115,42 +196,102 @@ class ReaderMenuState(
}, },
isInOverflow = false, isInOverflow = false,
iconButtonText = tabLabel, iconButtonText = tabLabel,
testingTag = TAB_MENU_ITEM_TESTING_TAG testingTag = TAB_MENU_ITEM_TESTING_TAG,
customView = { TabSwitcherBadge(tabLabel = tabLabel) }
) )
} }
}
if (urlIsValid) { @Composable
list += listOf( fun TabSwitcherBadge(tabLabel: String, modifier: Modifier = Modifier) {
ActionMenuItem( Box(
icon = IconItem.Drawable(R.drawable.ic_add_note), modifier = modifier
contentDescription = R.string.take_notes, .size(MATERIAL_MINIMUM_HEIGHT_AND_WIDTH)
onClick = { menuClickListener.onAddNoteMenuClicked() }, .padding(TWELVE_DP),
testingTag = TAKE_NOTE_MENU_ITEM_TESTING_TAG contentAlignment = Alignment.Center
), ) {
ActionMenuItem( Box(
contentDescription = R.string.menu_random_article, modifier = modifier
onClick = { menuClickListener.onRandomArticleMenuClicked() }, .clip(RoundedCornerShape(TAB_SWITCHER_CORNER_RADIUS))
testingTag = RANDOM_ARTICLE_MENU_ITEM_TESTING_TAG .background(Black)
), .border(ONE_DP, White, RoundedCornerShape(TAB_SWITCHER_CORNER_RADIUS))
ActionMenuItem( .padding(horizontal = SIX_DP, vertical = TWO_DP)
contentDescription = R.string.menu_full_screen, .defaultMinSize(minWidth = TWENTY_DP, minHeight = TWENTY_DP),
onClick = { menuClickListener.onFullscreenMenuClicked() }, contentAlignment = Alignment.Center
testingTag = FULL_SCREEN_MENU_ITEM_TESTING_TAG ) {
) Text(
) text = tabLabel,
color = White,
if (!disableReadAloud) { fontWeight = FontWeight.Bold,
list += ActionMenuItem( fontSize = TAB_SWITCHER_TEXT_SIZE,
contentDescription = if (isReadingAloud) R.string.menu_read_aloud_stop else R.string.menu_read_aloud, maxLines = 1,
onClick = { overflow = TextOverflow.Ellipsis
isReadingAloud = !isReadingAloud
menuClickListener.onReadAloudMenuClicked()
},
testingTag = READ_ALOUD_MENU_ITEM_TESTING_TAG
) )
} }
} }
}
return list 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,
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,
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,
isInOverflow = true
)
}
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,
isInOverflow = true
)
}
}
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,15 +150,19 @@ fun ReaderScreen(
} else { } else {
ShowZIMFileContent(state) ShowZIMFileContent(state)
ShowProgressBarIfZIMFilePageIsLoading(state) ShowProgressBarIfZIMFilePageIsLoading(state)
TtsControls(state) Column(
BottomAppBarOfReaderScreen( modifier = Modifier.align(Alignment.BottomCenter)
state.bookmarkButtonItem, ) {
state.previousPageButtonItem, TtsControls(state)
state.onHomeButtonClick, BottomAppBarOfReaderScreen(
state.nextPageButtonItem, state.bookmarkButtonItem,
state.onTocClick, state.previousPageButtonItem,
state.shouldShowBottomAppBar state.onHomeButtonClick,
) state.nextPageButtonItem,
state.onTocClick,
state.shouldShowBottomAppBar
)
}
ShowFullScreenView(state) ShowFullScreenView(state)
} }
ShowDonationLayout(state) ShowDonationLayout(state)
@ -225,9 +229,9 @@ private fun NoBookOpenView(
} }
@Composable @Composable
private fun BoxScope.TtsControls(state: ReaderScreenState) { private fun TtsControls(state: ReaderScreenState) {
if (state.showTtsControls) { if (state.showTtsControls) {
Row(modifier = Modifier.align(Alignment.BottomCenter)) { Row {
Button( Button(
onClick = state.onPauseTtsClick, onClick = state.onPauseTtsClick,
modifier = Modifier modifier = Modifier
@ -235,7 +239,7 @@ private fun BoxScope.TtsControls(state: ReaderScreenState) {
.alpha(TTS_BUTTONS_CONTROL_ALPHA) .alpha(TTS_BUTTONS_CONTROL_ALPHA)
) { ) {
Text( Text(
text = state.pauseTtsButtonText, text = state.pauseTtsButtonText.uppercase(),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
@ -247,7 +251,7 @@ private fun BoxScope.TtsControls(state: ReaderScreenState) {
.alpha(TTS_BUTTONS_CONTROL_ALPHA) .alpha(TTS_BUTTONS_CONTROL_ALPHA)
) { ) {
Text( Text(
text = stringResource(R.string.stop), text = stringResource(R.string.stop).uppercase(),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
@ -276,7 +280,7 @@ private fun BackToTopFab(state: ReaderScreenState) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun BoxScope.BottomAppBarOfReaderScreen( private fun BottomAppBarOfReaderScreen(
bookmarkButtonItem: Triple<() -> Unit, () -> Unit, Drawable>, bookmarkButtonItem: Triple<() -> Unit, () -> Unit, Drawable>,
previousPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>, previousPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
onHomeButtonClick: () -> Unit, onHomeButtonClick: () -> Unit,
@ -288,7 +292,6 @@ private fun BoxScope.BottomAppBarOfReaderScreen(
BottomAppBar( BottomAppBar(
containerColor = Black, containerColor = Black,
contentColor = White, contentColor = White,
modifier = Modifier.align(Alignment.BottomCenter),
scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
) { ) {
Row( Row(

View File

@ -18,8 +18,10 @@
package org.kiwix.kiwixmobile.core.ui.components package org.kiwix.kiwixmobile.core.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
@ -135,44 +137,13 @@ private fun AppBarTitle(
) )
} }
@Suppress("LongMethod")
@Composable @Composable
private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) { private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
var overflowExpanded by remember { mutableStateOf(false) } var overflowExpanded by remember { mutableStateOf(false) }
Row { Row {
val (mainActions, overflowActions) = actionMenuItems.partition { !it.isInOverflow } val (mainActions, overflowActions) = actionMenuItems.partition { !it.isInOverflow }
mainActions.forEach { menuItem -> MainMenuItems(mainActions)
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,
)
}
}
}
if (overflowActions.isNotEmpty()) { if (overflowActions.isNotEmpty()) {
IconButton(onClick = { overflowExpanded = true }) { IconButton(onClick = { overflowExpanded = true }) {
Icon( Icon(
@ -182,22 +153,77 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
) )
} }
} }
DropdownMenu( OverflowMenuItems(overflowExpanded, overflowActions) { overflowExpanded = false }
expanded = overflowExpanded, }
onDismissRequest = { overflowExpanded = false } }
) {
overflowActions.forEach { menuItem -> @Composable
DropdownMenuItem( private fun MainMenuItems(mainActions: List<ActionMenuItem>) {
text = { mainActions.forEach { menuItem ->
Text(text = menuItem.iconButtonText) val modifier = menuItem.modifier.testTag(menuItem.testingTag)
},
onClick = { menuItem.customView?.let { customComposable ->
overflowExpanded = false Box(modifier = modifier.clickable(enabled = menuItem.isEnabled) { menuItem.onClick() }) {
menuItem.onClick() customComposable()
},
enabled = menuItem.isEnabled
)
} }
} ?: 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 = onDismiss
) {
overflowActions.forEachIndexed { index, menuItem ->
DropdownMenuItem(
text = {
Column {
Text(
text = menuItem.iconButtonText.ifEmpty {
stringResource(id = menuItem.contentDescription)
}
)
}
},
onClick = {
onDismiss()
menuItem.onClick()
},
enabled = menuItem.isEnabled
)
} }
} }
} }

View File

@ -19,6 +19,7 @@
package org.kiwix.kiwixmobile.core.ui.models package org.kiwix.kiwixmobile.core.ui.models
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import org.kiwix.kiwixmobile.core.ui.theme.White import org.kiwix.kiwixmobile.core.ui.theme.White
@ -32,5 +33,6 @@ data class ActionMenuItem(
val iconButtonText: String = "", val iconButtonText: String = "",
val testingTag: String, val testingTag: String,
val modifier: Modifier = Modifier, 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 val READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE = 30.dp
const val TTS_BUTTONS_CONTROL_ALPHA = 0.6f const val TTS_BUTTONS_CONTROL_ALPHA = 0.6f
val CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING = 24.dp 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 * provided configuration. It takes into account whether read aloud and tabs are enabled or disabled
* and creates the menu accordingly. * and creates the menu accordingly.
*/ */
override fun createMainMenu(menu: Menu?): ReaderMenuState? = override fun createMainMenu(): ReaderMenuState =
ReaderMenuState( ReaderMenuState(
this, this,
isUrlValidInitially = urlIsValid(),
disableReadAloud = BuildConfig.DISABLE_READ_ALOUD, disableReadAloud = BuildConfig.DISABLE_READ_ALOUD,
disableTabs = BuildConfig.DISABLE_TABS, disableTabs = BuildConfig.DISABLE_TABS,
disableSearch = BuildConfig.DISABLE_TITLE disableSearch = BuildConfig.DISABLE_TITLE