From f7ceaf8d0d3c9b519e61a1542643219f42e87cff Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 16:14:22 +0700 Subject: [PATCH 1/9] suppress ContextCastToActivity --- .../main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt index b1d1327..7f1beb5 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt @@ -1,5 +1,6 @@ package com.paulcoding.hviewer.ui.page.posts +import android.annotation.SuppressLint import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween @@ -64,6 +65,7 @@ fun FavoriteCard( } } +@SuppressLint("ContextCastToActivity") @OptIn(ExperimentalMaterial3Api::class) @Composable fun PostCard( From cf2400ddae2b14a1a9bab7a029289aeee762ccea Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 17:22:31 +0700 Subject: [PATCH 2/9] Add Tabs page --- .../com/paulcoding/hviewer/model/PostModel.kt | 11 ++- .../com/paulcoding/hviewer/model/SiteModel.kt | 8 +- .../paulcoding/hviewer/ui/page/AppEntry.kt | 17 ++-- .../hviewer/ui/page/AppViewModel.kt | 22 +++++ .../com/paulcoding/hviewer/ui/page/Route.kt | 1 + .../hviewer/ui/page/posts/PostCard.kt | 9 +- .../hviewer/ui/page/posts/PostsPage.kt | 13 +++ .../hviewer/ui/page/tabs/TabsPage.kt | 95 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 9 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt diff --git a/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt b/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt index d8bca57..1aa994a 100644 --- a/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt @@ -22,7 +22,16 @@ data class PostItem( val size: Int? = null, val views: Int? = null, val quantity: Int? = null, -) +) { + private fun getHost(): String { + return url.split("/")[2] + } + + fun getSiteConfig(hostsMap: Map): SiteConfig? { + val host = getHost() + return hostsMap[host] + } +} // duplicated? @Entity(tableName = "history") diff --git a/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt b/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt index fc757c7..7fc7920 100644 --- a/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt @@ -36,4 +36,10 @@ data class SiteConfig( data class SiteConfigs( val version: Int = 1, val sites: Map = mapOf() -) \ No newline at end of file +) { + fun toHostsMap(): Map { + return sites.map { + it.value.baseUrl.split('/')[2] to it.value + }.toMap() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt index e1bf3b5..fc975a6 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt @@ -34,6 +34,7 @@ import com.paulcoding.hviewer.ui.page.posts.PostsPage import com.paulcoding.hviewer.ui.page.search.SearchPage import com.paulcoding.hviewer.ui.page.settings.SettingsPage import com.paulcoding.hviewer.ui.page.sites.SitesPage +import com.paulcoding.hviewer.ui.page.tabs.TabsPage import com.paulcoding.hviewer.ui.page.web.WebPage @Composable @@ -88,12 +89,12 @@ fun AppEntry() { navController.navigate(Route.LIST_SCRIPT + "/crash_log") }, onLockEnabled = { - navController.navigate(Route.LOCK) { - popUpTo(navController.graph.startDestinationId) { inclusive = true } - launchSingleTop = true - restoreState = false - } - }, goBack = { navController.popBackStack() }) + navController.navigate(Route.LOCK) { + popUpTo(navController.graph.startDestinationId) { inclusive = true } + launchSingleTop = true + restoreState = false + } + }, goBack = { navController.popBackStack() }) } animatedComposable(Route.POSTS) { PostsPage( @@ -103,6 +104,7 @@ fun AppEntry() { }, navToSearch = { navController.navigate(Route.SEARCH) }, navToCustomTag = { navToCustomTag(it) }, + navToTabs = { navController.navigate(Route.TABS) }, goBack = { navController.popBackStack() }, ) } @@ -199,6 +201,9 @@ fun AppEntry() { val url = appViewModel.getWebViewUrl() WebPage(goBack = { navController.popBackStack() }, url = url) } + animatedComposable(Route.TABS) { + TabsPage(goBack = { navController.popBackStack() }, appViewModel = appViewModel,siteConfigs = siteConfigs) + } } } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt index e770a08..631f104 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt @@ -28,6 +28,9 @@ class AppViewModel : ViewModel() { private var _stateFlow = MutableStateFlow(UiState()) val stateFlow = _stateFlow.asStateFlow() + private var _tabs = MutableStateFlow(listOf()) + val tabs = _tabs.asStateFlow() + val favoritePosts = DatabaseProvider.getInstance().favoritePostDao().getAll() val historyPosts = DatabaseProvider.getInstance().historyDao().getAll() @@ -107,4 +110,23 @@ class AppViewModel : ViewModel() { DatabaseProvider.getInstance().historyDao().delete(history) } } + + fun addTab(postItem: PostItem) { + if (!_tabs.value.contains(postItem)) + _tabs.update { + it + postItem + } + } + + fun removeTab(postItem: PostItem) { + _tabs.update { + it - postItem + } + } + + fun clearTabs() { + _tabs.update { + emptyList() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt index 101a5e3..8334bef 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/Route.kt @@ -15,4 +15,5 @@ object Route { const val LOCK = "lock" const val HISTORY = "history" const val WEBVIEW = "webview" + const val TABS = "tabs" } \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt index 7f1beb5..14ba92e 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Tab import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -49,11 +50,13 @@ fun FavoriteCard( isFavorite: Boolean = false, setFavorite: (Boolean) -> Unit = {}, onTagClick: (Tag) -> Unit = {}, + onAddToTabs: () -> Unit = {}, onClick: () -> Unit, ) { PostCard( postItem = postItem, onTagClick = onTagClick, + onAddToTabs = onAddToTabs, onClick = onClick ) { HFavoriteIcon( @@ -72,6 +75,7 @@ fun PostCard( postItem: PostItem, onTagClick: (Tag) -> Unit = {}, onClick: () -> Unit, + onAddToTabs: () -> Unit = {}, content: @Composable BoxScope.() -> Unit = {}, ) { var isBottomSheetVisible by remember { mutableStateOf(false) } @@ -88,8 +92,6 @@ fun PostCard( Card( elevation = CardDefaults.cardElevation(4.dp), - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 12.dp), border = CardDefaults.outlinedCardBorder(), shape = CardDefaults.outlinedShape, onClick = { onClick() }, @@ -110,6 +112,9 @@ fun PostCard( HIcon(Icons.Outlined.Info) { isBottomSheetVisible = true } + HIcon(Icons.Outlined.Tab, modifier = Modifier.align(Alignment.TopCenter)) { + onAddToTabs() + } content() } } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostsPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostsPage.kt index 5572d28..6e54af9 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostsPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostsPage.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.Tab import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -32,8 +33,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.paulcoding.hviewer.MainApp.Companion.appContext +import com.paulcoding.hviewer.R import com.paulcoding.hviewer.extensions.isScrolledToEnd import com.paulcoding.hviewer.extensions.toCapital +import com.paulcoding.hviewer.helper.makeToast import com.paulcoding.hviewer.model.PostItem import com.paulcoding.hviewer.model.SiteConfig import com.paulcoding.hviewer.model.Tag @@ -54,9 +57,12 @@ fun PostsPage( navToImages: (PostItem) -> Unit, navToSearch: () -> Unit, navToCustomTag: (Tag) -> Unit, + navToTabs: () -> Unit, goBack: () -> Unit ) { val appState by appViewModel.stateFlow.collectAsState() + val tabs by appViewModel.tabs.collectAsState(initial = listOf()) + val siteConfig = appState.site.second val listTag: List = siteConfig.tags.keys.map { key -> @@ -74,6 +80,9 @@ fun PostsPage( }, actions = { HPageProgress(pageProgress.first, pageProgress.second) HIcon(imageVector = Icons.Outlined.Search) { navToSearch() } + if (tabs.isNotEmpty()) { + HIcon(imageVector = Icons.Outlined.Tab) { navToTabs() } + } }) }) { paddings -> Column(modifier = Modifier.padding(paddings)) { @@ -166,6 +175,10 @@ fun PageContent( onTagClick = { navToCustomTag(it) }, + onAddToTabs = { + appViewModel.addTab(post) + makeToast(R.string.added_to_tabs) + }, setFavorite = { isFavorite -> if (isFavorite) appViewModel.addFavorite(post) 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 new file mode 100644 index 0000000..7c0c262 --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt @@ -0,0 +1,95 @@ +package com.paulcoding.hviewer.ui.page.tabs + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.paulcoding.hviewer.R +import com.paulcoding.hviewer.model.SiteConfigs +import com.paulcoding.hviewer.ui.component.HBackIcon +import com.paulcoding.hviewer.ui.component.HEmpty +import com.paulcoding.hviewer.ui.page.AppViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@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() } } + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.tabs)) }, + navigationIcon = { + HBackIcon { goBack() } + }, + ) + } + ) { paddings -> + Column(modifier = Modifier.padding(paddings)) { + if (tabs.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + HEmpty() + } + } else { + ScrollableTabRow( + selectedTabIndex = selectedTabIndex, + modifier = Modifier.fillMaxWidth(), + edgePadding = 0.dp, + ) { + tabs.forEachIndexed { index, tab -> + val siteConfig = tab.getSiteConfig(hostsMap) + Tab( + selected = selectedTabIndex == index, + selectedContentColor = MaterialTheme.colorScheme.primary, + unselectedContentColor = MaterialTheme.colorScheme.outline, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(text = tab.url) }, + ) + } + } + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { pageIndex -> + val tab = tabs[pageIndex] + Text(tab.url) + } + } + } + } +} + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e88394..738ebc4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,4 +29,6 @@ Undo Open crash logs Edit local scripts + Added to tabs + Tabs \ No newline at end of file From 57e5167641da10bd95eefc9427ffa51982103acf Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 20:28:56 +0700 Subject: [PATCH 3/9] Split to ImageList component --- .../paulcoding/hviewer/ui/page/post/Images.kt | 208 ++++++++++++++++++ .../hviewer/ui/page/post/PostPage.kt | 199 +---------------- 2 files changed, 211 insertions(+), 196 deletions(-) create mode 100644 app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt 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 new file mode 100644 index 0000000..728929a --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt @@ -0,0 +1,208 @@ +package com.paulcoding.hviewer.ui.page.post + +import android.annotation.SuppressLint +import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.viewmodel.compose.viewModel +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.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 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) { + val viewModel: PostViewModel = viewModel( + factory = PostViewModelFactory(postUrl, siteConfig = siteConfig) + ) + + val uiState by viewModel.stateFlow.collectAsState() + var selectedImage by remember { mutableStateOf(null) } + val listState = rememberLazyListState() + + LaunchedEffect(uiState.error) { + uiState.error?.let { + Toast.makeText(appContext, it.message ?: it.toString(), Toast.LENGTH_SHORT).show() + } + } + + LaunchedEffect(Unit) { + viewModel.getImages() + } + + LaunchedEffect(listState.firstVisibleItemIndex) { + if (viewModel.canLoadMorePostData() && !uiState.isLoading && listState.isScrolledToEnd()) { + viewModel.getNextImages() + } + } + + HideSystemBars() + + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.images, key = { it }) { image -> + PostImage(url = image) { + selectedImage = image + } + } + if (uiState.isLoading) + item { + Box( + modifier = Modifier.statusBarsPadding() + ) { + HLoading() + } + } + } + + HGoTop(listState) + + AnimatedVisibility( + listState.isScrollingUp().value, + modifier = Modifier + .align(Alignment.TopStart) + .padding(16.dp) + .statusBarsPadding() + .statusBarsPadding(), + enter = fadeInWithBlur(), + exit = fadeOutWithBlur(), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + HBackIcon { goBack() } + Text("${uiState.postPage}/${uiState.postTotalPage}") + } + } + + if (selectedImage != null) { + ImageModal(url = selectedImage!!) { + selectedImage = null + } + } + } +} + +@Composable +fun ImageModal(url: String, dismiss: () -> Unit) { + val zoomableState = rememberZoomableState(ZoomSpec(maxZoomFactor = 5f)) + + val doubleClickToZoomListener = + DoubleClickToZoomListener { _, _ -> + dismiss() + } + + Dialog( + onDismissRequest = { dismiss() }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Box( + modifier = Modifier + .zoomable( + state = zoomableState, + onClick = { makeToast(R.string.double_click_to_dismiss) }, + onDoubleClick = doubleClickToZoomListener + ) + ) { + HImage( + url, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } +} + +@SuppressLint("ContextCastToActivity") +@Composable +fun PostImage(url: String, onTap: () -> Unit = {}) { + val showMenu = remember { mutableStateOf(false) } + val menuOffset = remember { mutableStateOf(Pair(0f, 0f)) } + val context = LocalContext.current as MainActivity + + Box(modifier = Modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { offset -> + println("pressed $url") + showMenu.value = true + menuOffset.value = Pair(offset.x, offset.y) + }, + onTap = { onTap() } + ) + }) { + HImage( + url = url + ) + + DropdownMenu( + expanded = showMenu.value, + onDismissRequest = { showMenu.value = false }, + ) { + DropdownMenuItem( + onClick = { + showMenu.value = false + context.openInBrowser(url) + }, + text = { + Text(stringResource(R.string.open_in_browser)) + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt index c61de4e..461dc19 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt @@ -1,211 +1,18 @@ package com.paulcoding.hviewer.ui.page.post -import android.annotation.SuppressLint -import android.widget.Toast -import androidx.compose.animation.AnimatedVisibility -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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -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.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -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.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel -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.ui.component.HBackIcon -import com.paulcoding.hviewer.ui.component.HGoTop -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.AppViewModel -import com.paulcoding.hviewer.ui.page.fadeInWithBlur -import com.paulcoding.hviewer.ui.page.fadeOutWithBlur -import me.saket.telephoto.zoomable.DoubleClickToZoomListener -import me.saket.telephoto.zoomable.ZoomSpec -import me.saket.telephoto.zoomable.rememberZoomableState -import me.saket.telephoto.zoomable.zoomable -@SuppressLint("UseOfNonLambdaOffsetOverload") @Composable fun PostPage(appViewModel: AppViewModel, navToWebView: (String) -> Unit, goBack: () -> Unit) { val appState by appViewModel.stateFlow.collectAsState() val post = appState.post val siteConfig = appState.site.second - val viewModel: PostViewModel = viewModel( - factory = PostViewModelFactory(post.url, siteConfig = siteConfig) + ImageList( + postUrl = post.url, siteConfig = siteConfig, + goBack = goBack ) - - val uiState by viewModel.stateFlow.collectAsState() - var selectedImage by remember { mutableStateOf(null) } - val listState = rememberLazyListState() - - LaunchedEffect(uiState.error) { - uiState.error?.let { - Toast.makeText(appContext, it.message ?: it.toString(), Toast.LENGTH_SHORT).show() - } - } - - LaunchedEffect(Unit) { - viewModel.getImages() - } - - LaunchedEffect(listState.firstVisibleItemIndex) { - if (viewModel.canLoadMorePostData() && !uiState.isLoading && listState.isScrolledToEnd()) { - viewModel.getNextImages() - } - } - - HideSystemBars() - - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - state = listState, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(uiState.images, key = { it }) { image -> - PostImage(url = image) { - selectedImage = image - } - } - if (uiState.isLoading) - item { - Box( - modifier = Modifier.statusBarsPadding() - ) { - HLoading() - } - } - } - - HGoTop(listState) - - AnimatedVisibility( - listState.isScrollingUp().value, - modifier = Modifier - .align(Alignment.TopStart) - .padding(16.dp) - .statusBarsPadding() - .statusBarsPadding(), - enter = fadeInWithBlur(), - exit = fadeOutWithBlur(), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - HBackIcon { goBack() } - Text("${uiState.postPage}/${uiState.postTotalPage}") - } - } - - if (selectedImage != null) { - ImageModal(url = selectedImage!!) { - selectedImage = null - } - } - } } - -@Composable -fun ImageModal(url: String, dismiss: () -> Unit) { - val zoomableState = rememberZoomableState(ZoomSpec(maxZoomFactor = 5f)) - - val doubleClickToZoomListener = - DoubleClickToZoomListener { _, _ -> - dismiss() - } - - Dialog( - onDismissRequest = { dismiss() }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - Surface( - shape = RoundedCornerShape(16.dp), - modifier = Modifier - .fillMaxWidth(), - ) { - Box( - modifier = Modifier - .zoomable( - state = zoomableState, - onClick = { makeToast(R.string.double_click_to_dismiss) }, - onDoubleClick = doubleClickToZoomListener - ) - ) { - HImage( - url, - modifier = Modifier.align(Alignment.Center), - ) - } - } - } -} - -@Composable -fun PostImage(url: String, onTap: () -> Unit = {}) { - val showMenu = remember { mutableStateOf(false) } - val menuOffset = remember { mutableStateOf(Pair(0f, 0f)) } - val context = LocalContext.current as MainActivity - - Box(modifier = Modifier.pointerInput(Unit) { - detectTapGestures( - onLongPress = { offset -> - println("pressed $url") - showMenu.value = true - menuOffset.value = Pair(offset.x, offset.y) - }, - onTap = { onTap() } - ) - }) { - HImage( - url = url - ) - - DropdownMenu( - expanded = showMenu.value, - onDismissRequest = { showMenu.value = false }, - ) { - DropdownMenuItem( - onClick = { - showMenu.value = false - context.openInBrowser(url) - }, - text = { - Text(stringResource(R.string.open_in_browser)) - } - ) - } - } -} \ No newline at end of file From 808d9ee6ceb3e27bb51f7c13a12f81f460734090 Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 20:48:55 +0700 Subject: [PATCH 4/9] Add ImageList to TabsPage --- .../com/paulcoding/hviewer/model/PostModel.kt | 4 ++-- .../paulcoding/hviewer/ui/page/post/Images.kt | 1 + .../hviewer/ui/page/tabs/TabsPage.kt | 20 +++++++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt b/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt index 1aa994a..c92276b 100644 --- a/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/model/PostModel.kt @@ -23,7 +23,7 @@ data class PostItem( val views: Int? = null, val quantity: Int? = null, ) { - private fun getHost(): String { + fun getHost(): String { return url.split("/")[2] } @@ -47,7 +47,7 @@ data class PostHistory( val views: Int? = null, val quantity: Int? = null, ) { - fun toPostItem():PostItem { + fun toPostItem(): PostItem { return PostItem( url = url, name = name, 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 728929a..99fcc57 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 @@ -59,6 +59,7 @@ import me.saket.telephoto.zoomable.zoomable @Composable fun ImageList(postUrl: String, siteConfig: SiteConfig, goBack: () -> Unit) { val viewModel: PostViewModel = viewModel( + key = postUrl, factory = PostViewModelFactory(postUrl, siteConfig = siteConfig) ) 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 7c0c262..27b98cd 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 @@ -21,13 +21,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.paulcoding.hviewer.R +import com.paulcoding.hviewer.helper.alsoLog import com.paulcoding.hviewer.model.SiteConfigs import com.paulcoding.hviewer.ui.component.HBackIcon import com.paulcoding.hviewer.ui.component.HEmpty import com.paulcoding.hviewer.ui.page.AppViewModel +import com.paulcoding.hviewer.ui.page.post.ImageList import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -66,7 +69,6 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo edgePadding = 0.dp, ) { tabs.forEachIndexed { index, tab -> - val siteConfig = tab.getSiteConfig(hostsMap) Tab( selected = selectedTabIndex == index, selectedContentColor = MaterialTheme.colorScheme.primary, @@ -76,7 +78,7 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo pagerState.animateScrollToPage(index) } }, - text = { Text(text = tab.url) }, + text = { Text(text = tab.getHost()) }, ) } } @@ -84,9 +86,19 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), + key = { tabs[it].url } ) { pageIndex -> - val tab = tabs[pageIndex] - Text(tab.url) + val tab = tabs[pageIndex].alsoLog("tab") + val siteConfig = tab.getSiteConfig(hostsMap).alsoLog("siteConfig") + + if (siteConfig != null) + ImageList(tab.url, siteConfig = siteConfig, goBack = goBack) + else + Text( + "Site config not found for ${tab.url}", + modifier = Modifier.padding(16.dp), + color = Color.Red + ) } } } From c58415f1aceb2770fa30dd3fa15fcd1d5b5085ef Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 20:51:50 +0700 Subject: [PATCH 5/9] Make onAddToTabs optional --- .../com/paulcoding/hviewer/ui/page/posts/PostCard.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt index 14ba92e..41587aa 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt @@ -50,7 +50,7 @@ fun FavoriteCard( isFavorite: Boolean = false, setFavorite: (Boolean) -> Unit = {}, onTagClick: (Tag) -> Unit = {}, - onAddToTabs: () -> Unit = {}, + onAddToTabs: (() -> Unit)? = null, onClick: () -> Unit, ) { PostCard( @@ -75,7 +75,7 @@ fun PostCard( postItem: PostItem, onTagClick: (Tag) -> Unit = {}, onClick: () -> Unit, - onAddToTabs: () -> Unit = {}, + onAddToTabs: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit = {}, ) { var isBottomSheetVisible by remember { mutableStateOf(false) } @@ -112,9 +112,10 @@ fun PostCard( HIcon(Icons.Outlined.Info) { isBottomSheetVisible = true } - HIcon(Icons.Outlined.Tab, modifier = Modifier.align(Alignment.TopCenter)) { - onAddToTabs() - } + if (onAddToTabs != null) + HIcon(Icons.Outlined.Tab, modifier = Modifier.align(Alignment.TopCenter)) { + onAddToTabs() + } content() } } From 2ef46967af2998fd3aa57f84eedba747fbdf0016 Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 21:01:00 +0700 Subject: [PATCH 6/9] Update OpenInNew icon for tabs --- .../java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt index 41587aa..63ecb0c 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/posts/PostCard.kt @@ -12,8 +12,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.Tab import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -113,7 +113,10 @@ fun PostCard( isBottomSheetVisible = true } if (onAddToTabs != null) - HIcon(Icons.Outlined.Tab, modifier = Modifier.align(Alignment.TopCenter)) { + HIcon( + Icons.AutoMirrored.Outlined.OpenInNew, + modifier = Modifier.align(Alignment.TopCenter) + ) { onAddToTabs() } content() From f770ae9c3eabf15556c9b7a1892999539bb575e9 Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 21:02:36 +0700 Subject: [PATCH 7/9] Remove Scaffold for TabsPage --- .../hviewer/ui/page/tabs/TabsPage.kt | 103 +++++++----------- 1 file changed, 42 insertions(+), 61 deletions(-) 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 27b98cd..59b04f7 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 @@ -7,13 +7,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf @@ -22,18 +19,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.paulcoding.hviewer.R import com.paulcoding.hviewer.helper.alsoLog import com.paulcoding.hviewer.model.SiteConfigs -import com.paulcoding.hviewer.ui.component.HBackIcon import com.paulcoding.hviewer.ui.component.HEmpty import com.paulcoding.hviewer.ui.page.AppViewModel import com.paulcoding.hviewer.ui.page.post.ImageList import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteConfigs) { appViewModel.stateFlow @@ -43,63 +36,51 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo val scope = rememberCoroutineScope() val hostsMap by remember { derivedStateOf { siteConfigs.toHostsMap() } } - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.tabs)) }, - navigationIcon = { - HBackIcon { goBack() } - }, - ) - } - ) { paddings -> - Column(modifier = Modifier.padding(paddings)) { - if (tabs.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - ) { - 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()) }, - ) - } + Column { + if (tabs.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + 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(), - key = { tabs[it].url } - ) { pageIndex -> - val tab = tabs[pageIndex].alsoLog("tab") - val siteConfig = tab.getSiteConfig(hostsMap).alsoLog("siteConfig") + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + key = { tabs[it].url } + ) { pageIndex -> + val tab = tabs[pageIndex].alsoLog("tab") + val siteConfig = tab.getSiteConfig(hostsMap).alsoLog("siteConfig") - if (siteConfig != null) - ImageList(tab.url, siteConfig = siteConfig, goBack = goBack) - else - Text( - "Site config not found for ${tab.url}", - modifier = Modifier.padding(16.dp), - color = Color.Red - ) - } + if (siteConfig != null) + ImageList(tab.url, siteConfig = siteConfig, goBack = goBack) + else + Text( + "Site config not found for ${tab.url}", + modifier = Modifier.padding(16.dp), + color = Color.Red + ) } } } From 0a87145a25194120dfa9ab89a8ba7094272968fa Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 21:09:52 +0700 Subject: [PATCH 8/9] Remove Log --- .../java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 59b04f7..084c0da 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 @@ -20,7 +20,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.paulcoding.hviewer.helper.alsoLog import com.paulcoding.hviewer.model.SiteConfigs import com.paulcoding.hviewer.ui.component.HEmpty import com.paulcoding.hviewer.ui.page.AppViewModel @@ -70,8 +69,8 @@ fun TabsPage(goBack: () -> Unit, appViewModel: AppViewModel, siteConfigs: SiteCo modifier = Modifier.fillMaxSize(), key = { tabs[it].url } ) { pageIndex -> - val tab = tabs[pageIndex].alsoLog("tab") - val siteConfig = tab.getSiteConfig(hostsMap).alsoLog("siteConfig") + val tab = tabs[pageIndex] + val siteConfig = tab.getSiteConfig(hostsMap) if (siteConfig != null) ImageList(tab.url, siteConfig = siteConfig, goBack = goBack) From 135a6fe30be023b43811bf6216390e39ac8d5f43 Mon Sep 17 00:00:00 2001 From: paulcoding810 Date: Thu, 20 Feb 2025 21:48:23 +0700 Subject: [PATCH 9/9] Clear tabs on go back from TabsPage --- app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt index fc975a6..5c3f980 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt @@ -202,7 +202,10 @@ fun AppEntry() { WebPage(goBack = { navController.popBackStack() }, url = url) } animatedComposable(Route.TABS) { - TabsPage(goBack = { navController.popBackStack() }, appViewModel = appViewModel,siteConfigs = siteConfigs) + TabsPage(goBack = { + navController.popBackStack() + appViewModel.clearTabs() + }, appViewModel = appViewModel, siteConfigs = siteConfigs) } } }