From f654a64b7efb09f08221dcd554f489fc8133c857 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Wed, 18 Jun 2025 02:23:32 +0530 Subject: [PATCH] Refactored all functionalities of the Reader's menu: showing or hiding menu items based on business logic, updating the tab item count, and more. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .../destination/reader/KiwixReaderFragment.kt | 4 +- core/detekt_baseline.xml | 3 +- .../core/main/reader/CoreReaderFragment.kt | 101 ++++--- .../core/main/reader/ReaderMenuState.kt | 249 ++++++++++++++---- .../core/main/reader/ReaderScreen.kt | 33 +-- .../core/ui/components/KiwixAppBar.kt | 120 +++++---- .../core/ui/models/ActionMenuItem.kt | 4 +- .../kiwixmobile/core/utils/ComposeDimens.kt | 2 + .../custom/main/CustomReaderFragment.kt | 3 +- 9 files changed, 359 insertions(+), 160 deletions(-) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt index 089ffe209..c2cbaeb3a 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt @@ -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() } } diff --git a/core/detekt_baseline.xml b/core/detekt_baseline.xml index 09091af73..c20fc03cd 100644 --- a/core/detekt_baseline.xml +++ b/core/detekt_baseline.xml @@ -13,7 +13,7 @@ LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList<KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean ) 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" ) 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 ) - 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 ) + LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup?, videoView: ViewGroup?, webViewClient: CoreWebViewClient, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null ) MagicNumber:ArticleCount.kt$ArticleCount$3 MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100 MagicNumber:DownloadItem.kt$DownloadItem$1000L @@ -23,6 +23,7 @@ MagicNumber:JNIInitialiser.kt$JNIInitialiser$1024 MagicNumber:Byte.kt$Byte$1024.0 MagicNumber:MainMenu.kt$MainMenu$99 + MagicNumber:ReaderMenuState.kt$ReaderMenuState$99 MagicNumber:OnSwipeTouchListener.kt$OnSwipeTouchListener.GestureListener$100 MagicNumber:SearchResultGenerator.kt$ZimSearchResultGenerator$200 MagicNumber:Seconds.kt$Seconds$24 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt index d06897d1d..bf87ad94c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt @@ -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,28 +1725,26 @@ abstract class CoreReaderFragment : @Suppress("NestedBlockDepth") override fun onReadAloudMenuClicked() { if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { - ttsControls?.let { ttsControls -> - when (ttsControls.visibility) { - GONE -> { - if (isBackToTopEnabled) { - backToTopButton?.hide() - } - if (tts?.isInitialized == false) { - isReadSelection = false - tts?.initializeTTS() - } else { - startReadAloud() - } - } - - VISIBLE -> { - if (isBackToTopEnabled) { - backToTopButton?.show() - } - tts?.stop() - } - - else -> {} + if (readerScreenState.value.showTtsControls) { + // currently TTS is running + if (isBackToTopEnabled) { + 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 + tts?.initializeTTS() + } else { + startReadAloud() } } } else { @@ -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() } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt index abdf44096..bc03c5b9d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt @@ -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() + + private val menuItemVisibility = mutableMapOf().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 { - if (isInTabSwitcher) { - return emptyList() - } + fun showBookSpecificMenuItems() { + setVisibility( + true, + MenuItemType.Search, + MenuItemType.TabSwitcher, + MenuItemType.RandomArticle, + MenuItemType.AddNote, + MenuItemType.ReadAloud + ) + } - val list = mutableListOf() + 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( - icon = IconItem.Drawable(R.drawable.ic_add_note), - contentDescription = R.string.take_notes, - onClick = { menuClickListener.onAddNoteMenuClicked() }, - testingTag = TAKE_NOTE_MENU_ITEM_TESTING_TAG - ), - ActionMenuItem( - contentDescription = R.string.menu_random_article, - onClick = { menuClickListener.onRandomArticleMenuClicked() }, - testingTag = RANDOM_ARTICLE_MENU_ITEM_TESTING_TAG - ), - ActionMenuItem( - contentDescription = R.string.menu_full_screen, - onClick = { menuClickListener.onFullscreenMenuClicked() }, - testingTag = FULL_SCREEN_MENU_ITEM_TESTING_TAG - ) - ) - - if (!disableReadAloud) { - list += 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 + @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 ) } } + } - 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 +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt index 5d43ef8c1..9673e6652 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt @@ -150,15 +150,19 @@ fun ReaderScreen( } else { ShowZIMFileContent(state) ShowProgressBarIfZIMFilePageIsLoading(state) - TtsControls(state) - BottomAppBarOfReaderScreen( - state.bookmarkButtonItem, - state.previousPageButtonItem, - state.onHomeButtonClick, - state.nextPageButtonItem, - state.onTocClick, - state.shouldShowBottomAppBar - ) + Column( + modifier = Modifier.align(Alignment.BottomCenter) + ) { + TtsControls(state) + BottomAppBarOfReaderScreen( + state.bookmarkButtonItem, + state.previousPageButtonItem, + state.onHomeButtonClick, + state.nextPageButtonItem, + 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( diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt index 59eead867..710c896d3 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt @@ -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) { 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,22 +153,77 @@ private fun ActionMenu(actionMenuItems: List) { ) } } - DropdownMenu( - expanded = overflowExpanded, - onDismissRequest = { overflowExpanded = false } - ) { - overflowActions.forEach { menuItem -> - DropdownMenuItem( - text = { - Text(text = menuItem.iconButtonText) - }, - onClick = { - overflowExpanded = false - menuItem.onClick() - }, - enabled = menuItem.isEnabled - ) + OverflowMenuItems(overflowExpanded, overflowActions) { overflowExpanded = false } + } +} + +@Composable +private fun MainMenuItems(mainActions: List) { + 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, + 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 + ) } } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt index ae5b7ea0f..4c9fa935e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt @@ -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 ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt index 8529c2a14..2c81cff90 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt @@ -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 } diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt index c79ce3c82..77d79383c 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt @@ -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