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:
MohitMaliFtechiz 2025-06-19 02:55:45 +05:30
parent f654a64b7e
commit b694ae3170
6 changed files with 362 additions and 143 deletions

View File

@ -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() {

View File

@ -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)

View File

@ -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,

View File

@ -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)
}

View File

@ -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,
)

View File

@ -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
}