Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}
}
}
93 changes: 66 additions & 27 deletions app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@ 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.RowScope
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
Expand All @@ -25,12 +33,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
Expand All @@ -39,25 +49,28 @@ 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
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)
Expand All @@ -66,6 +79,12 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) {
val uiState by viewModel.stateFlow.collectAsState()
var selectedImage by remember { mutableStateOf<String?>(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 {
Expand All @@ -83,7 +102,7 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) {
}
}

HideSystemBars()
SystemBar(uiState.isSystemBarHidden)

Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
Expand All @@ -92,6 +111,7 @@ fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) {
) {
items(uiState.images, key = { it }) { image ->
PostImage(url = image) {
viewModel.toggleSystemBarHidden()
selectedImage = image
}
}
Expand All @@ -105,33 +125,52 @@ 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
) {
bottomRowActions()
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
// }
// }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
Expand Down
82 changes: 48 additions & 34 deletions app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,48 +23,25 @@ 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
fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteConfigs) {
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(),
Expand All @@ -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}",
Expand All @@ -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()
)
}
}
}