From f523eba37c56537572c3a788c1f3e2e263c77f64 Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Fri, 21 Feb 2025 20:31:17 +0700 Subject: [PATCH 1/2] Update system bar handler --- .../hviewer/ui/component/HideSystemBars.kt | 34 ++++++++ .../paulcoding/hviewer/ui/page/post/Images.kt | 84 +++++++++++++------ .../hviewer/ui/page/post/PostViewModel.kt | 5 ++ 3 files changed, 97 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/component/HideSystemBars.kt b/app/src/main/java/com/paulcoding/hviewer/ui/component/HideSystemBars.kt index c7894ff..b7f28db 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/component/HideSystemBars.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/component/HideSystemBars.kt @@ -1,8 +1,11 @@ package com.paulcoding.hviewer.ui.component +import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.paulcoding.hviewer.MainActivity @@ -26,4 +29,35 @@ fun HideSystemBars() { controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT } } +} + +@Composable +fun SystemBar(isHidden: Boolean) { + val context = LocalContext.current + val window = (context as? ComponentActivity)?.window + val view = LocalView.current + + fun hideSystemBars() { + window?.let { + val controller = WindowInsetsControllerCompat(it, it.decorView) + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + fun showSystemBars() { + window?.let { + val controller = WindowInsetsControllerCompat(it, view) + controller.show(WindowInsetsCompat.Type.systemBars()) + } + } + + LaunchedEffect(isHidden) { + if (isHidden) { + hideSystemBars() + } else { + showSystemBars() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt index 99fcc57..d25f4e4 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt @@ -2,21 +2,28 @@ package com.paulcoding.hviewer.ui.page.post import android.annotation.SuppressLint import android.widget.Toast -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.KeyboardArrowUp import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,12 +32,14 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -39,17 +48,15 @@ import com.paulcoding.hviewer.MainActivity import com.paulcoding.hviewer.MainApp.Companion.appContext import com.paulcoding.hviewer.R import com.paulcoding.hviewer.extensions.isScrolledToEnd -import com.paulcoding.hviewer.extensions.isScrollingUp import com.paulcoding.hviewer.extensions.openInBrowser import com.paulcoding.hviewer.helper.makeToast import com.paulcoding.hviewer.model.SiteConfig import com.paulcoding.hviewer.ui.component.HBackIcon -import com.paulcoding.hviewer.ui.component.HGoTop +import com.paulcoding.hviewer.ui.component.HIcon import com.paulcoding.hviewer.ui.component.HImage import com.paulcoding.hviewer.ui.component.HLoading -import com.paulcoding.hviewer.ui.component.HideSystemBars -import com.paulcoding.hviewer.ui.page.fadeInWithBlur -import com.paulcoding.hviewer.ui.page.fadeOutWithBlur +import com.paulcoding.hviewer.ui.component.SystemBar +import kotlinx.coroutines.launch import me.saket.telephoto.zoomable.DoubleClickToZoomListener import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.rememberZoomableState @@ -66,6 +73,12 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) { val uiState by viewModel.stateFlow.collectAsState() var selectedImage by remember { mutableStateOf(null) } val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + val translationY by animateDpAsState( + targetValue = if (uiState.isSystemBarHidden) (-100).dp else 0.dp, + animationSpec = tween(200) + ) LaunchedEffect(uiState.error) { uiState.error?.let { @@ -83,7 +96,7 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) { } } - HideSystemBars() + SystemBar(uiState.isSystemBarHidden) Box(modifier = Modifier.fillMaxSize()) { LazyColumn( @@ -92,6 +105,7 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) { ) { items(uiState.images, key = { it }) { image -> PostImage(url = image) { + viewModel.toggleSystemBarHidden() selectedImage = image } } @@ -105,33 +119,51 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) { } } - HGoTop(listState) - - AnimatedVisibility( - listState.isScrollingUp().value, + Row( modifier = Modifier - .align(Alignment.TopStart) + .fillMaxWidth() + .offset { + IntOffset(x = 0, y = translationY.roundToPx()) + } .padding(16.dp) - .statusBarsPadding() .statusBarsPadding(), - enter = fadeInWithBlur(), - exit = fadeOutWithBlur(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - HBackIcon { goBack() } - Text("${uiState.postPage}/${uiState.postTotalPage}") - } + HBackIcon { goBack() } + Text("${uiState.postPage}/${uiState.postTotalPage}") } - if (selectedImage != null) { - ImageModal(url = selectedImage!!) { - selectedImage = null + Row( + modifier = Modifier + .fillMaxWidth() + .offset { + IntOffset(x = 0, y = -translationY.roundToPx()) + } + .align(Alignment.BottomStart) + .padding(16.dp) + .navigationBarsPadding(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Spacer(modifier = Modifier.weight(1f)) + HIcon( + Icons.Outlined.KeyboardArrowUp, + size = 32, + tint = MaterialTheme.colorScheme.primary, + rounded = true + ) { + scope.launch { + listState.animateScrollToItem(0, 0) + } } } + +// if (selectedImage != null) { +// ImageModal(url = selectedImage!!) { +// selectedImage = null +// } +// } } } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostViewModel.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostViewModel.kt index 5c9d388..9754be8 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostViewModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostViewModel.kt @@ -29,6 +29,7 @@ class PostViewModel(private val postUrl: String, siteConfig: SiteConfig) : ViewM val isLoading: Boolean = false, val error: Throwable? = null, val currentPostUrl: String = "", + val isSystemBarHidden: Boolean = true, ) private fun setError(th: Throwable) { @@ -79,6 +80,10 @@ class PostViewModel(private val postUrl: String, siteConfig: SiteConfig) : ViewM fun canLoadMorePostData(): Boolean { return _stateFlow.value.postPage < _stateFlow.value.postTotalPage } + + fun toggleSystemBarHidden() { + _stateFlow.update { it.copy(isSystemBarHidden = !it.isSystemBarHidden) } + } } @Suppress("UNCHECKED_CAST") From f864f3ce8b0906e5a56da785b03de4058520af4a Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Fri, 21 Feb 2025 21:24:10 +0700 Subject: [PATCH 2/2] Replace TabsPage's ScrollableTabRow by nav arrow icons --- .../paulcoding/hviewer/ui/component/HIcon.kt | 2 + .../paulcoding/hviewer/ui/page/post/Images.kt | 9 +- .../hviewer/ui/page/tabs/TabsPage.kt | 82 +++++++++++-------- 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/component/HIcon.kt b/app/src/main/java/com/paulcoding/hviewer/ui/component/HIcon.kt index 76cef22..0ea4521 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/component/HIcon.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/component/HIcon.kt @@ -52,10 +52,12 @@ fun HIcon( modifier: Modifier = Modifier, tint: Color = LocalContentColor.current, rounded: Boolean = false, + enabled: Boolean = true, onClick: () -> Unit ) { IconButton( onClick = { onClick() }, + enabled = enabled, modifier = if (rounded) modifier .clip(CircleShape) .background(Color.White) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt index d25f4e4..a79c706 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -64,7 +65,12 @@ import me.saket.telephoto.zoomable.zoomable @Composable -fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) { +fun ImageList( + postUrl: String, + siteConfig: SiteConfig, + goBack: () -> Unit, + bottomRowActions: @Composable (RowScope.() -> Unit) = {}, +) { val viewModel: PostViewModel = viewModel( key = postUrl, factory = PostViewModelFactory(postUrl, siteConfig = siteConfig) @@ -146,6 +152,7 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { + bottomRowActions() Spacer(modifier = Modifier.weight(1f)) HIcon( Icons.Outlined.KeyboardArrowUp, diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt index 084c0da..f9893a4 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt @@ -1,15 +1,16 @@ package com.paulcoding.hviewer.ui.page.tabs import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ChevronLeft +import androidx.compose.material.icons.outlined.ChevronRight import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -22,8 +23,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.paulcoding.hviewer.model.SiteConfigs import com.paulcoding.hviewer.ui.component.HEmpty +import com.paulcoding.hviewer.ui.component.HIcon import com.paulcoding.hviewer.ui.page.AppViewModel import com.paulcoding.hviewer.ui.page.post.ImageList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable @@ -31,39 +34,14 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo appViewModel.stateFlow val tabs by appViewModel.tabs.collectAsState(initial = listOf()) val pagerState = rememberPagerState { tabs.size } - val selectedTabIndex by remember { derivedStateOf { pagerState.currentPage } } val scope = rememberCoroutineScope() val hostsMap by remember { derivedStateOf { siteConfigs.toHostsMap() } } - Column { + + Box(modifier = Modifier.fillMaxSize()) { if (tabs.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - ) { - HEmpty() - } + HEmpty() } else { - ScrollableTabRow( - selectedTabIndex = selectedTabIndex, - modifier = Modifier.fillMaxWidth(), - edgePadding = 0.dp, - ) { - tabs.forEachIndexed { index, tab -> - Tab( - selected = selectedTabIndex == index, - selectedContentColor = MaterialTheme.colorScheme.primary, - unselectedContentColor = MaterialTheme.colorScheme.outline, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(text = tab.getHost()) }, - ) - } - } - HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), @@ -73,7 +51,14 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo val siteConfig = tab.getSiteConfig(hostsMap) if (siteConfig != null) - ImageList(tab.url, siteConfig = siteConfig, goBack = goBack) + ImageList( + tab.url, + siteConfig = siteConfig, + goBack = goBack, + bottomRowActions = { + BottomRowActions(pageIndex, scope, pagerState) + } + ) else Text( "Site config not found for ${tab.url}", @@ -85,3 +70,32 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo } } +@Composable +internal fun BottomRowActions(pageIndex: Int, scope: CoroutineScope, pagerState: PagerState) { + HIcon( + Icons.Outlined.ChevronLeft, + size = 32, + rounded = true, + enabled = pageIndex > 0 + ) { + scope.launch { + pagerState.animateScrollToPage( + pageIndex.dec() + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + HIcon( + Icons.Outlined.ChevronRight, + size = 32, + rounded = true, + enabled = pageIndex < pagerState.pageCount - 1 + ) { + scope.launch { + pagerState.animateScrollToPage( + pageIndex.inc() + ) + } + } +} +