diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt index c2cbaeb3a..062330781 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt @@ -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() { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt index bf87ad94c..dbdc9cf7a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/CoreReaderFragment.kt @@ -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) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt index bc03c5b9d..170157123 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderMenuState.kt @@ -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, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt index 9673e6652..064e1fb5a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreen.kt @@ -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, + 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, 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 { + 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) +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt index 0bb43d980..d66cfdcb4 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/reader/ReaderScreenState.kt @@ -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, + /** + * 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, ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt index 2c81cff90..dc9bc7dfc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt @@ -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 }