mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
Refactored all tab-related functionalities.
* Improved the tabs UI to align with our XML-based layout. Added a border to the selected tab for better visual feedback, making it easier for users to identify the active tab. * Added animation to smoothly scroll to the selected tab when the tab switcher is opened. * Fixed a crash scenario when "Close All Tabs" was triggered. * Fixed a crash that occurred when launching the app for the first time. * Fixed an issue where the tab menu item was still visible even after all tabs were closed.
This commit is contained in:
parent
f654a64b7e
commit
b694ae3170
@ -42,6 +42,7 @@ import org.kiwix.kiwixmobile.core.R.string
|
||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super
|
||||
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super.ShouldCall
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setupDrawerToggle
|
||||
import org.kiwix.kiwixmobile.core.extensions.coreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.extensions.isFileExist
|
||||
@ -208,6 +209,26 @@ class KiwixReaderFragment : CoreReaderFragment() {
|
||||
selectTab(currentWebViewIndex)
|
||||
}
|
||||
}
|
||||
actionBar?.setDisplayShowTitleEnabled(true)
|
||||
toolbar?.let { activity?.setupDrawerToggle(it, true) }
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
if (webViewList.isEmpty()) {
|
||||
readerMenuState?.hideTabSwitcher()
|
||||
exitBook(shouldCloseZimBook)
|
||||
} else {
|
||||
// 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 main app.
|
||||
// setTopMarginToWebViews(0)
|
||||
readerScreenState.update {
|
||||
copy(
|
||||
shouldShowBottomAppBar = true,
|
||||
pageLoadingItem = false to ZERO,
|
||||
)
|
||||
}
|
||||
readerMenuState?.showWebViewOptions(urlIsValid())
|
||||
selectTab(currentWebViewIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setFragmentContainerBottomMarginToSizeOfNavBar() {
|
||||
|
@ -72,8 +72,10 @@ import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
@ -184,7 +186,6 @@ import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
|
||||
import org.kiwix.kiwixmobile.core.ui.components.rememberBottomNavigationVisibility
|
||||
import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
||||
import org.kiwix.kiwixmobile.core.utils.AnimationUtils.rotate
|
||||
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
|
||||
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getWindowWidth
|
||||
import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler
|
||||
import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler.ShowDonationDialogCallback
|
||||
@ -291,7 +292,7 @@ abstract class CoreReaderFragment :
|
||||
@JvmField
|
||||
@Inject
|
||||
var painter: DarkModeViewPainter? = null
|
||||
protected var currentWebViewIndex = 0
|
||||
protected var currentWebViewIndex by mutableStateOf(0)
|
||||
private var currentTtsWebViewIndex = 0
|
||||
protected var actionBar: ActionBar? = null
|
||||
protected var mainMenu: MainMenu? = null
|
||||
@ -403,7 +404,23 @@ abstract class CoreReaderFragment :
|
||||
bottomNavigationHeight = ZERO,
|
||||
shouldShowBottomAppBar = true,
|
||||
selectedWebView = null,
|
||||
readerScreenTitle = ""
|
||||
readerScreenTitle = "",
|
||||
showTabSwitcher = false,
|
||||
darkModeViewPainter = null,
|
||||
currentWebViewPosition = ZERO,
|
||||
onTabClickListener = object : TabClickListener {
|
||||
override fun onSelectTab(position: Int) {
|
||||
hideTabSwitcher()
|
||||
selectTab(position)
|
||||
|
||||
// Bug Fix #592
|
||||
updateBottomToolbarArrowsAlpha()
|
||||
}
|
||||
|
||||
override fun onCloseTab(position: Int) {
|
||||
closeTab(position)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
private var readerLifeCycleScope: CoroutineScope? = null
|
||||
@ -506,7 +523,7 @@ abstract class CoreReaderFragment :
|
||||
val lazyListState = rememberLazyListState()
|
||||
val isBottomNavVisible = rememberBottomNavigationVisibility(lazyListState)
|
||||
LaunchedEffect(isBottomNavVisible) {
|
||||
(requireActivity() as CoreMainActivity).toggleBottomNavigation(isBottomNavVisible)
|
||||
(activity as? CoreMainActivity)?.toggleBottomNavigation(isBottomNavVisible)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { webViewList.size }
|
||||
@ -515,11 +532,14 @@ abstract class CoreReaderFragment :
|
||||
updateTabIcon(size)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(currentWebViewIndex, readerMenuState?.isInTabSwitcher) {
|
||||
readerScreenState.update {
|
||||
copy(
|
||||
bottomNavigationHeight = getBottomNavigationHeight(),
|
||||
readerScreenTitle = context.getString(R.string.reader)
|
||||
readerScreenTitle = context.getString(R.string.reader),
|
||||
darkModeViewPainter = darkModeViewPainter,
|
||||
currentWebViewPosition = currentWebViewIndex,
|
||||
showTabSwitcher = readerMenuState?.isInTabSwitcher == true
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -528,8 +548,8 @@ abstract class CoreReaderFragment :
|
||||
actionMenuItems = readerMenuState?.menuItems.orEmpty(),
|
||||
navigationIcon = {
|
||||
NavigationIcon(
|
||||
iconItem = IconItem.Vector(Icons.Filled.Menu),
|
||||
contentDescription = string.open_drawer,
|
||||
iconItem = navigationIcon(),
|
||||
contentDescription = navigationIconContentDescription(),
|
||||
onClick = { navigationIconClick() }
|
||||
)
|
||||
},
|
||||
@ -640,18 +660,35 @@ abstract class CoreReaderFragment :
|
||||
|
||||
private fun getBottomNavigationHeight(): Int = getBottomNavigationView()?.measuredHeight ?: ZERO
|
||||
|
||||
private fun navigationIconClick() {
|
||||
// Manually handle the navigation open/close.
|
||||
// Since currently we are using the view based navigation drawer in other screens.
|
||||
// Once we fully migrate to jetpack compose we will refactor this code to use the
|
||||
// compose navigation.
|
||||
// TODO Replace with compose based navigation when migration is done.
|
||||
val activity = activity as CoreMainActivity
|
||||
if (activity.navigationDrawerIsOpen()) {
|
||||
activity.closeNavigationDrawer()
|
||||
private fun navigationIconContentDescription() =
|
||||
if (readerMenuState?.isInTabSwitcher == true) {
|
||||
R.string.search_open_in_new_tab
|
||||
} else {
|
||||
activity.openNavigationDrawer()
|
||||
string.open_drawer
|
||||
}
|
||||
|
||||
private fun navigationIconClick() {
|
||||
if (readerMenuState?.isInTabSwitcher == true) {
|
||||
onHomeMenuClicked()
|
||||
} else {
|
||||
// Manually handle the navigation open/close.
|
||||
// Since currently we are using the view based navigation drawer in other screens.
|
||||
// Once we fully migrate to jetpack compose we will refactor this code to use the
|
||||
// compose navigation.
|
||||
// TODO Replace with compose based navigation when migration is done.
|
||||
val activity = activity as CoreMainActivity
|
||||
if (activity.navigationDrawerIsOpen()) {
|
||||
activity.closeNavigationDrawer()
|
||||
} else {
|
||||
activity.openNavigationDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigationIcon() = if (readerMenuState?.isInTabSwitcher == true) {
|
||||
IconItem.Drawable(R.drawable.ic_round_add_white_36dp)
|
||||
} else {
|
||||
IconItem.Vector(Icons.Filled.Menu)
|
||||
}
|
||||
|
||||
private fun addAlertDialogToDialogHost() {
|
||||
@ -936,10 +973,14 @@ abstract class CoreReaderFragment :
|
||||
setIsCloseAllTabButtonClickable(true)
|
||||
// Set a negative top margin to the web views to remove
|
||||
// the unwanted blank space caused by the toolbar.
|
||||
setTopMarginToWebViews(-requireActivity().getToolbarHeight())
|
||||
// setTopMarginToWebViews(-requireActivity().getToolbarHeight())
|
||||
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
readerScreenState.update {
|
||||
copy(shouldShowBottomAppBar = false)
|
||||
copy(
|
||||
shouldShowBottomAppBar = false,
|
||||
pageLoadingItem = false to ZERO,
|
||||
readerScreenTitle = ""
|
||||
)
|
||||
}
|
||||
contentFrame?.visibility = GONE
|
||||
progressBar?.visibility = GONE
|
||||
@ -1030,10 +1071,16 @@ abstract class CoreReaderFragment :
|
||||
}
|
||||
progressBar?.hide()
|
||||
selectTab(currentWebViewIndex)
|
||||
readerScreenState.update {
|
||||
copy(
|
||||
shouldShowBottomAppBar = true,
|
||||
pageLoadingItem = false to ZERO,
|
||||
)
|
||||
}
|
||||
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)
|
||||
// setTopMarginToWebViews(0)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1587,11 +1634,10 @@ abstract class CoreReaderFragment :
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
requireActivity().getString(R.string.tab_closed),
|
||||
actionLabel = requireActivity().getString(R.string.undo),
|
||||
snackbarDuration = SnackbarDuration.Long,
|
||||
actionClick = { restoreDeletedTab(index) },
|
||||
lifecycleScope = lifecycleScope,
|
||||
snackBarResult = { result ->
|
||||
if (result != SnackbarResult.Dismissed && webViewList.isEmpty() && isAdded) {
|
||||
if (result == SnackbarResult.Dismissed && webViewList.isEmpty() && isAdded) {
|
||||
closeZimBook()
|
||||
}
|
||||
}
|
||||
@ -1610,7 +1656,7 @@ abstract class CoreReaderFragment :
|
||||
readerScreenState.update {
|
||||
copy(
|
||||
shouldShowBottomAppBar = false,
|
||||
readerScreenTitle = requireActivity().getString(R.string.reader)
|
||||
readerScreenTitle = context?.getString(R.string.reader).orEmpty()
|
||||
)
|
||||
}
|
||||
contentFrame?.visibility = GONE
|
||||
@ -1657,7 +1703,7 @@ abstract class CoreReaderFragment :
|
||||
webViewList.add(index, it)
|
||||
tabsAdapter?.notifyDataSetChanged()
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
requireActivity().getString(R.string.tab_restored),
|
||||
context?.getString(R.string.tab_restored).orEmpty(),
|
||||
lifecycleScope = lifecycleScope
|
||||
)
|
||||
setUpWithTextToSpeech(it)
|
||||
@ -1778,16 +1824,15 @@ abstract class CoreReaderFragment :
|
||||
}
|
||||
|
||||
override fun onHomeMenuClicked() {
|
||||
if (tabSwitcherRoot?.visibility == VISIBLE) {
|
||||
if (readerScreenState.value.showTabSwitcher) {
|
||||
hideTabSwitcher()
|
||||
}
|
||||
createNewTab()
|
||||
}
|
||||
|
||||
override fun onTabMenuClicked() {
|
||||
if (tabSwitcherRoot?.visibility == VISIBLE) {
|
||||
if (readerScreenState.value.showTabSwitcher) {
|
||||
hideTabSwitcher()
|
||||
selectTab(currentWebViewIndex)
|
||||
} else {
|
||||
showTabSwitcher()
|
||||
}
|
||||
@ -2062,8 +2107,8 @@ abstract class CoreReaderFragment :
|
||||
}
|
||||
} else {
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
requireActivity().getString(R.string.request_storage),
|
||||
actionLabel = requireActivity().getString(R.string.menu_settings),
|
||||
context?.getString(R.string.request_storage).orEmpty(),
|
||||
context?.getString(R.string.menu_settings),
|
||||
snackbarDuration = SnackbarDuration.Long,
|
||||
actionClick = {
|
||||
val intent = Intent()
|
||||
@ -2100,13 +2145,12 @@ abstract class CoreReaderFragment :
|
||||
tabsAdapter?.notifyDataSetChanged()
|
||||
openHomeScreen()
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
requireActivity().getString(R.string.tabs_closed),
|
||||
actionLabel = requireActivity().getString(R.string.undo),
|
||||
snackbarDuration = SnackbarDuration.Long,
|
||||
context?.getString(R.string.tabs_closed).orEmpty(),
|
||||
context?.getString(R.string.undo),
|
||||
actionClick = { restoreDeletedTabs() },
|
||||
lifecycleScope = lifecycleScope,
|
||||
snackBarResult = { result ->
|
||||
if (result != SnackbarResult.Dismissed && webViewList.isEmpty() && isAdded) {
|
||||
if (result == SnackbarResult.Dismissed && webViewList.isEmpty() && isAdded) {
|
||||
closeZimBook()
|
||||
}
|
||||
}
|
||||
@ -2122,7 +2166,7 @@ abstract class CoreReaderFragment :
|
||||
webViewList.addAll(tempWebViewListForUndo)
|
||||
tabsAdapter?.notifyDataSetChanged()
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
requireActivity().getString(R.string.tabs_restored),
|
||||
context?.getString(R.string.tabs_restored).orEmpty(),
|
||||
lifecycleScope = lifecycleScope
|
||||
)
|
||||
reopenBook()
|
||||
@ -2162,7 +2206,7 @@ abstract class CoreReaderFragment :
|
||||
if (isBookmarked) {
|
||||
repositoryActions?.deleteBookmark(libKiwixBook.id, articleUrl)
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
requireActivity().getString(R.string.bookmark_removed),
|
||||
context?.getString(R.string.bookmark_removed).orEmpty(),
|
||||
lifecycleScope = lifecycleScope
|
||||
)
|
||||
} else {
|
||||
@ -2171,9 +2215,9 @@ abstract class CoreReaderFragment :
|
||||
LibkiwixBookmarkItem(it, articleUrl, zimFileReader, libKiwixBook)
|
||||
)
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
requireActivity().getString(R.string.bookmark_added),
|
||||
context?.getString(R.string.bookmark_added).orEmpty(),
|
||||
lifecycleScope = lifecycleScope,
|
||||
actionLabel = requireActivity().getString(R.string.open),
|
||||
actionLabel = context?.getString(R.string.open),
|
||||
actionClick = { goToBookmarks() }
|
||||
)
|
||||
}
|
||||
@ -2890,9 +2934,9 @@ abstract class CoreReaderFragment :
|
||||
if (isOpenNewTabInBackground) {
|
||||
newTabInBackground(url)
|
||||
readerScreenState.value.snackBarHostState.snack(
|
||||
message = requireActivity().getString(R.string.new_tab_snack_bar),
|
||||
message = context?.getString(R.string.new_tab_snack_bar).orEmpty(),
|
||||
lifecycleScope = lifecycleScope,
|
||||
actionLabel = requireActivity().getString(R.string.open),
|
||||
actionLabel = context?.getString(R.string.open),
|
||||
actionClick = {
|
||||
if (webViewList.size > 1) {
|
||||
selectTab(webViewList.size - 1)
|
||||
|
@ -46,7 +46,7 @@ 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_ICON_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
|
||||
@ -105,7 +105,7 @@ class ReaderMenuState(
|
||||
}
|
||||
|
||||
fun showWebViewOptions(valid: Boolean) {
|
||||
isInTabSwitcher = false
|
||||
hideTabSwitcher()
|
||||
urlIsValid = valid
|
||||
setVisibility(
|
||||
urlIsValid,
|
||||
@ -165,6 +165,10 @@ class ReaderMenuState(
|
||||
)
|
||||
}
|
||||
|
||||
fun hideTabSwitcher() {
|
||||
isInTabSwitcher = false
|
||||
}
|
||||
|
||||
private fun updateMenuItems() {
|
||||
menuItems.clear()
|
||||
addSearchMenuItem()
|
||||
@ -185,15 +189,12 @@ class ReaderMenuState(
|
||||
}
|
||||
|
||||
private fun addTabMenuItem() {
|
||||
if (!disableTabs && urlIsValid) {
|
||||
if (!disableTabs && urlIsValid && webViewCount > 0) {
|
||||
val tabLabel = if (webViewCount > 99) ":D" else "$webViewCount"
|
||||
menuItems += ActionMenuItem(
|
||||
icon = null,
|
||||
contentDescription = R.string.switch_tabs,
|
||||
onClick = {
|
||||
isInTabSwitcher = true
|
||||
menuClickListener.onTabMenuClicked()
|
||||
},
|
||||
onClick = { menuClickListener.onTabMenuClicked() },
|
||||
isInOverflow = false,
|
||||
iconButtonText = tabLabel,
|
||||
testingTag = TAB_MENU_ITEM_TESTING_TAG,
|
||||
@ -212,9 +213,9 @@ class ReaderMenuState(
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(TAB_SWITCHER_CORNER_RADIUS))
|
||||
.clip(RoundedCornerShape(TAB_SWITCHER_ICON_CORNER_RADIUS))
|
||||
.background(Black)
|
||||
.border(ONE_DP, White, RoundedCornerShape(TAB_SWITCHER_CORNER_RADIUS))
|
||||
.border(ONE_DP, White, RoundedCornerShape(TAB_SWITCHER_ICON_CORNER_RADIUS))
|
||||
.padding(horizontal = SIX_DP, vertical = TWO_DP)
|
||||
.defaultMinSize(minWidth = TWENTY_DP, minHeight = TWENTY_DP),
|
||||
contentAlignment = Alignment.Center
|
||||
@ -232,9 +233,7 @@ class ReaderMenuState(
|
||||
}
|
||||
|
||||
private fun addReaderMenuItems() {
|
||||
if (!urlIsValid) return
|
||||
|
||||
if (menuItemVisibility[MenuItemType.Search] == true) {
|
||||
if (menuItemVisibility[MenuItemType.AddNote] == true) {
|
||||
menuItems += ActionMenuItem(
|
||||
icon = IconItem.Drawable(R.drawable.ic_add_note),
|
||||
contentDescription = R.string.take_notes,
|
||||
|
@ -18,8 +18,12 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.main.reader
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@ -30,7 +34,6 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -38,10 +41,10 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
@ -58,19 +61,24 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -80,7 +88,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import kotlinx.coroutines.delay
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
|
||||
import org.kiwix.kiwixmobile.core.main.DarkModeViewPainter
|
||||
import org.kiwix.kiwixmobile.core.main.KiwixWebView
|
||||
@ -99,6 +109,8 @@ import org.kiwix.kiwixmobile.core.ui.theme.Black
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.KiwixDialogTheme
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_TAB_ICON_ANIMATION_TIMEOUT
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_TAB_ICON_SIZE
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONE_DP
|
||||
@ -127,11 +139,11 @@ fun ReaderScreen(
|
||||
Scaffold(
|
||||
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
|
||||
topBar = {
|
||||
KiwixAppBar(
|
||||
state.readerScreenTitle,
|
||||
navigationIcon,
|
||||
ReaderTopBar(
|
||||
state,
|
||||
actionMenuItems,
|
||||
scrollBehavior
|
||||
scrollBehavior,
|
||||
navigationIcon
|
||||
)
|
||||
},
|
||||
floatingActionButton = { BackToTopFab(state) },
|
||||
@ -140,34 +152,63 @@ fun ReaderScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(bottom = bottomNavHeight.value)
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
if (state.isNoBookOpenInReader) {
|
||||
NoBookOpenView(state.onOpenLibraryButtonClicked)
|
||||
} else {
|
||||
ShowZIMFileContent(state)
|
||||
ShowProgressBarIfZIMFilePageIsLoading(state)
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
) {
|
||||
TtsControls(state)
|
||||
BottomAppBarOfReaderScreen(
|
||||
state.bookmarkButtonItem,
|
||||
state.previousPageButtonItem,
|
||||
state.onHomeButtonClick,
|
||||
state.nextPageButtonItem,
|
||||
state.onTocClick,
|
||||
state.shouldShowBottomAppBar
|
||||
)
|
||||
}
|
||||
ShowFullScreenView(state)
|
||||
ReaderContentLayout(state, Modifier.padding(paddingValues))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("ComposableLambdaParameterNaming")
|
||||
@Composable
|
||||
private fun ReaderTopBar(
|
||||
state: ReaderScreenState,
|
||||
actionMenuItems: List<ActionMenuItem>,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
navigationIcon: @Composable () -> Unit,
|
||||
) {
|
||||
if (!state.fullScreenItem.first) {
|
||||
KiwixAppBar(
|
||||
title = if (state.showTabSwitcher) "" else state.readerScreenTitle,
|
||||
navigationIcon = navigationIcon,
|
||||
actionMenuItems = actionMenuItems,
|
||||
topAppBarScrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReaderContentLayout(state: ReaderScreenState, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
when {
|
||||
state.showTabSwitcher -> TabSwitcherView(
|
||||
state.kiwixWebViewList,
|
||||
state.currentWebViewPosition,
|
||||
state.onTabClickListener,
|
||||
state.onCloseAllTabs,
|
||||
state.darkModeViewPainter
|
||||
)
|
||||
|
||||
state.isNoBookOpenInReader -> NoBookOpenView(state.onOpenLibraryButtonClicked)
|
||||
|
||||
else -> {
|
||||
ShowZIMFileContent(state)
|
||||
ShowProgressBarIfZIMFilePageIsLoading(state)
|
||||
Column(Modifier.align(Alignment.BottomCenter)) {
|
||||
TtsControls(state)
|
||||
BottomAppBarOfReaderScreen(
|
||||
state.bookmarkButtonItem,
|
||||
state.previousPageButtonItem,
|
||||
state.onHomeButtonClick,
|
||||
state.nextPageButtonItem,
|
||||
state.onTocClick,
|
||||
state.shouldShowBottomAppBar
|
||||
)
|
||||
}
|
||||
ShowDonationLayout(state)
|
||||
ShowFullScreenView(state)
|
||||
}
|
||||
}
|
||||
|
||||
ShowDonationLayout(state)
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,11 +419,11 @@ private fun BoxScope.ShowDonationLayout(state: ReaderScreenState) {
|
||||
fun TabSwitcherView(
|
||||
webViews: List<KiwixWebView>,
|
||||
selectedIndex: Int,
|
||||
onSelectTab: (Int) -> Unit,
|
||||
onCloseTab: (Int) -> Unit,
|
||||
onTabClickListener: TabClickListener,
|
||||
onCloseAllTabs: () -> Unit,
|
||||
painter: DarkModeViewPainter
|
||||
painter: DarkModeViewPainter?
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
@ -390,7 +431,8 @@ fun TabSwitcherView(
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = SIXTEEN_DP),
|
||||
contentPadding = PaddingValues(horizontal = SIXTEEN_DP, vertical = EIGHT_DP),
|
||||
horizontalArrangement = Arrangement.spacedBy(EIGHT_DP)
|
||||
horizontalArrangement = Arrangement.spacedBy(EIGHT_DP),
|
||||
state = state
|
||||
) {
|
||||
itemsIndexed(webViews, key = { _, item -> item.hashCode() }) { index, webView ->
|
||||
val context = LocalContext.current
|
||||
@ -401,33 +443,76 @@ fun TabSwitcherView(
|
||||
|
||||
LaunchedEffect(webView) {
|
||||
if (title != context.getString(R.string.menu_home)) {
|
||||
painter.update(webView)
|
||||
painter?.update(webView)
|
||||
}
|
||||
}
|
||||
|
||||
TabItemView(
|
||||
index = index,
|
||||
title = title,
|
||||
isSelected = index == selectedIndex,
|
||||
webView = webView,
|
||||
onSelectTab = { onSelectTab(index) },
|
||||
onCloseTab = { onCloseTab(index) }
|
||||
onTabClickListener = onTabClickListener,
|
||||
)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
state.animateScrollToItem(selectedIndex)
|
||||
}
|
||||
CloseAllTabButton(onCloseAllTabs)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.CloseAllTabButton(onCloseAllTabs: () -> Unit) {
|
||||
var isAnimating by remember { mutableStateOf(false) }
|
||||
var isDone by remember { mutableStateOf(false) }
|
||||
|
||||
// Animate rotation from 0f to 360f
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (isAnimating) 360f else 0f,
|
||||
animationSpec = tween(durationMillis = 600),
|
||||
finishedListener = {
|
||||
isDone = true
|
||||
isAnimating = false
|
||||
}
|
||||
)
|
||||
|
||||
// ⏳ Auto-reset to close icon after delay
|
||||
LaunchedEffect(isDone) {
|
||||
if (isDone) {
|
||||
delay(CLOSE_TAB_ICON_ANIMATION_TIMEOUT)
|
||||
isDone = false
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = onCloseAllTabs,
|
||||
onClick = {
|
||||
isAnimating = true
|
||||
onCloseAllTabs()
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING)
|
||||
.graphicsLayer {
|
||||
rotationZ = rotation
|
||||
}
|
||||
.clickable(
|
||||
enabled = !isAnimating,
|
||||
onClick = {
|
||||
isAnimating = true
|
||||
onCloseAllTabs()
|
||||
}
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_close_black_24dp),
|
||||
painter = painterResource(
|
||||
id = if (isDone) {
|
||||
R.drawable.ic_done_white_24dp
|
||||
} else {
|
||||
R.drawable.ic_close_black_24dp
|
||||
}
|
||||
),
|
||||
contentDescription = stringResource(R.string.close_all_tabs)
|
||||
)
|
||||
}
|
||||
@ -436,71 +521,120 @@ private fun BoxScope.CloseAllTabButton(onCloseAllTabs: () -> Unit) {
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun TabItemView(
|
||||
index: Int,
|
||||
title: String,
|
||||
isSelected: Boolean,
|
||||
webView: KiwixWebView,
|
||||
modifier: Modifier = Modifier,
|
||||
onSelectTab: () -> Unit,
|
||||
onCloseTab: () -> Unit
|
||||
onTabClickListener: TabClickListener
|
||||
) {
|
||||
val cardElevation = if (isSelected) EIGHT_DP else TWO_DP
|
||||
val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
|
||||
|
||||
val (cardWidth, cardHeight) = getTabCardSize(toolbarHeightDp = 56.dp)
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = EIGHT_DP, vertical = FOUR_DP)
|
||||
.widthIn(min = 200.dp)
|
||||
.width(cardWidth)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = FOUR_DP),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = EIGHT_DP),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
IconButton(onClick = onCloseTab) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_clear_white_24dp),
|
||||
contentDescription = stringResource(R.string.close_tab)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Card with WebView (non-interactive with overlay)
|
||||
Card(
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
border = BorderStroke(ONE_DP, borderColor),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.6f) // approximate height logic
|
||||
.clickable { onSelectTab() }
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
// Detach if needed to avoid WebView already has a parent issue
|
||||
(webView.parent as? ViewGroup)?.removeView(webView)
|
||||
FrameLayout(context).apply {
|
||||
addView(webView)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
TabItemHeader(title, index, onTabClickListener)
|
||||
TabItemCard(
|
||||
webView,
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
onTabClickListener,
|
||||
borderColor,
|
||||
cardElevation,
|
||||
index
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabItemHeader(
|
||||
title: String,
|
||||
index: Int,
|
||||
onTabClickListener: TabClickListener
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = FOUR_DP),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.padding(end = FOUR_DP)
|
||||
.weight(1f),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
IconButton(
|
||||
onClick = { onTabClickListener.onCloseTab(index) },
|
||||
modifier = Modifier.size(CLOSE_TAB_ICON_SIZE)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_clear_white_24dp),
|
||||
contentDescription = stringResource(R.string.close_tab)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabItemCard(
|
||||
webView: KiwixWebView,
|
||||
cardWidth: Dp,
|
||||
cardHeight: Dp,
|
||||
onTabClickListener: TabClickListener,
|
||||
borderColor: Color,
|
||||
elevation: Dp,
|
||||
index: Int
|
||||
) {
|
||||
Card(
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = elevation),
|
||||
border = BorderStroke(ONE_DP, borderColor),
|
||||
shape = MaterialTheme.shapes.extraSmall,
|
||||
modifier = Modifier
|
||||
.width(cardWidth)
|
||||
.height(cardHeight)
|
||||
.clickable { onTabClickListener.onSelectTab(index) }
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
FrameLayout(context).apply {
|
||||
(webView.parent as? ViewGroup)?.removeView(webView)
|
||||
addView(webView)
|
||||
val clickableView = View(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
setOnClickListener { onTabClickListener.onSelectTab(index) }
|
||||
}
|
||||
addView(clickableView)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getTabCardSize(toolbarHeightDp: Dp): Pair<Dp, Dp> {
|
||||
val windowSize = LocalWindowInfo.current.containerSize
|
||||
val density = LocalDensity.current
|
||||
|
||||
val screenWidth = with(density) { windowSize.width.toDp() }
|
||||
val screenHeight = with(density) { windowSize.height.toDp() }
|
||||
|
||||
val cardWidth = screenWidth / 2
|
||||
val cardHeight = ((screenHeight - toolbarHeightDp) / 2).coerceAtLeast(HUNDERED.dp)
|
||||
|
||||
return cardWidth to cardHeight
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberScrollBehavior(
|
||||
bottomNavigationHeight: Int,
|
||||
@ -527,3 +661,8 @@ fun rememberScrollBehavior(
|
||||
|
||||
return bottomNavHeight to lazyListState
|
||||
}
|
||||
|
||||
interface TabClickListener {
|
||||
fun onSelectTab(position: Int)
|
||||
fun onCloseTab(position: Int)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.core.main.reader
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import org.kiwix.kiwixmobile.core.main.DarkModeViewPainter
|
||||
import org.kiwix.kiwixmobile.core.main.KiwixWebView
|
||||
import org.kiwix.kiwixmobile.core.ui.models.IconItem.Drawable
|
||||
|
||||
@ -79,10 +80,18 @@ data class ReaderScreenState(
|
||||
*/
|
||||
val pauseTtsButtonText: String,
|
||||
val onStopTtsClick: () -> Unit = {},
|
||||
/**
|
||||
* Holds the current selected webView position.
|
||||
*/
|
||||
val currentWebViewPosition: Int,
|
||||
/**
|
||||
* To show in the tabs view.
|
||||
*/
|
||||
val kiwixWebViewList: List<KiwixWebView>,
|
||||
/**
|
||||
* To show/hide tab switcher.
|
||||
*/
|
||||
val showTabSwitcher: Boolean,
|
||||
/**
|
||||
* Manages the showing of current selected webView.
|
||||
*/
|
||||
@ -131,5 +140,10 @@ data class ReaderScreenState(
|
||||
* Manages the showing of Reader's [BottomAppBarOfReaderScreen].
|
||||
*/
|
||||
val shouldShowBottomAppBar: Boolean,
|
||||
val readerScreenTitle: String
|
||||
val readerScreenTitle: String,
|
||||
val darkModeViewPainter: DarkModeViewPainter?,
|
||||
/**
|
||||
* Manages the click event on tabs.
|
||||
*/
|
||||
val onTabClickListener: TabClickListener,
|
||||
)
|
||||
|
@ -180,11 +180,13 @@ object ComposeDimens {
|
||||
val STORAGE_LOADING_PROGRESS_BAR_SIZE = 40.dp
|
||||
val CATEGORY_TITLE_TEXT_SIZE = 14.sp
|
||||
|
||||
// Reader screen dimes
|
||||
// Reader screen dimens
|
||||
val READER_BOTTOM_APP_BAR_LAYOUT_HEIGHT = 40.dp
|
||||
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
|
||||
const val TAB_SWITCHER_ICON_CORNER_RADIUS = 10
|
||||
val CLOSE_TAB_ICON_SIZE = 20.dp
|
||||
const val CLOSE_TAB_ICON_ANIMATION_TIMEOUT = 1200L
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user