mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
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:
parent
5c879484a6
commit
f654a64b7e
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList<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>
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,24 +153,79 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user