Basic Navigation

This commit is contained in:
tom5079
2024-03-27 00:21:16 -07:00
parent e8ba5c4881
commit 62948abf75
11 changed files with 236 additions and 155 deletions

View File

@@ -32,3 +32,4 @@
kotlinx.serialization.KSerializer serializer(...); kotlinx.serialization.KSerializer serializer(...);
} }
-keep class xyz.quaver.pupil.** { *; } -keep class xyz.quaver.pupil.** { *; }
-dontwarn org.slf4j.impl.StaticLoggerBinder

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.3.10", "versionName": "6.0.0",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@@ -27,10 +27,10 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.adaptive.calculateDisplayFeatures import com.google.accompanist.adaptive.calculateDisplayFeatures
import xyz.quaver.pupil.ui.composable.MainApp import xyz.quaver.pupil.ui.composable.MainApp
import xyz.quaver.pupil.ui.theme.AppTheme import xyz.quaver.pupil.ui.theme.AppTheme
import xyz.quaver.pupil.ui.viewmodel.MainUIState
import xyz.quaver.pupil.ui.viewmodel.MainViewModel import xyz.quaver.pupil.ui.viewmodel.MainViewModel
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
@@ -48,13 +48,15 @@ class MainActivity : BaseActivity() {
val windowSize = calculateWindowSizeClass(this) val windowSize = calculateWindowSizeClass(this)
val displayFeatures = calculateDisplayFeatures(this) val displayFeatures = calculateDisplayFeatures(this)
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.searchState.collectAsStateWithLifecycle()
val navController = rememberNavController()
MainApp( MainApp(
windowSize = windowSize, windowSize = windowSize,
displayFeatures = displayFeatures, displayFeatures = displayFeatures,
uiState = uiState, uiState = uiState,
navigateToDestination = viewModel::navigateToDestination, navController = navController,
closeDetailScreen = viewModel::closeDetailScreen, closeDetailScreen = viewModel::closeDetailScreen,
onQueryChange = viewModel::onQueryChange, onQueryChange = viewModel::onQueryChange,
loadSearchResult = viewModel::loadSearchResult loadSearchResult = viewModel::loadSearchResult

View File

@@ -1,7 +1,11 @@
package xyz.quaver.pupil.ui.composable package xyz.quaver.pupil.ui.composable
import android.util.Log
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -14,25 +18,39 @@ import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.PermanentNavigationDrawer import androidx.compose.material3.PermanentNavigationDrawer
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.activity
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.window.layout.DisplayFeature import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature import androidx.window.layout.FoldingFeature
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.pupil.R
import xyz.quaver.pupil.networking.SearchQuery import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.viewmodel.MainUIState import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.viewmodel.SearchState
@Composable @Composable
fun MainApp( fun MainApp(
windowSize: WindowSizeClass, windowSize: WindowSizeClass,
displayFeatures: List<DisplayFeature>, displayFeatures: List<DisplayFeature>,
uiState: MainUIState, uiState: SearchState,
navigateToDestination: (MainDestination) -> Unit, navController: NavHostController,
closeDetailScreen: () -> Unit, closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit, onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit loadSearchResult: (IntRange) -> Unit
@@ -87,7 +105,7 @@ fun MainApp(
displayFeatures, displayFeatures,
navigationContentPosition, navigationContentPosition,
uiState, uiState,
navigateToDestination, navController,
closeDetailScreen = closeDetailScreen, closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange, onQueryChange = onQueryChange,
loadSearchResult = loadSearchResult loadSearchResult = loadSearchResult
@@ -100,8 +118,8 @@ private fun MainNavigationWrapper(
contentType: ContentType, contentType: ContentType,
displayFeatures: List<DisplayFeature>, displayFeatures: List<DisplayFeature>,
navigationContentPosition: NavigationContentPosition, navigationContentPosition: NavigationContentPosition,
uiState: MainUIState, uiState: SearchState,
navigateToDestination: (MainDestination) -> Unit, navController: NavHostController,
closeDetailScreen: () -> Unit, closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit, onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit loadSearchResult: (IntRange) -> Unit
@@ -109,6 +127,9 @@ private fun MainNavigationWrapper(
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val openDrawer: () -> Unit = { val openDrawer: () -> Unit = {
coroutineScope.launch { coroutineScope.launch {
drawerState.open() drawerState.open()
@@ -119,9 +140,12 @@ private fun MainNavigationWrapper(
PermanentNavigationDrawer( PermanentNavigationDrawer(
drawerContent = { drawerContent = {
PermanentNavigationDrawerContent( PermanentNavigationDrawerContent(
selectedDestination = uiState.currentDestination, selectedDestination = currentRoute,
navigateToDestination = { navController.navigate(it.route) {
popUpTo(MainDestination.Search.route)
launchSingleTop = true
} },
navigationContentPosition = navigationContentPosition, navigationContentPosition = navigationContentPosition,
navigateToDestination = navigateToDestination
) )
} }
) { ) {
@@ -129,9 +153,8 @@ private fun MainNavigationWrapper(
navigationType = navigationType, navigationType = navigationType,
contentType = contentType, contentType = contentType,
displayFeatures = displayFeatures, displayFeatures = displayFeatures,
navigationContentPosition = navigationContentPosition,
uiState = uiState, uiState = uiState,
navigateToDestination = navigateToDestination, navController = navController,
onDrawerClicked = openDrawer, onDrawerClicked = openDrawer,
closeDetailScreen = closeDetailScreen, closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange, onQueryChange = onQueryChange,
@@ -142,9 +165,12 @@ private fun MainNavigationWrapper(
ModalNavigationDrawer( ModalNavigationDrawer(
drawerContent = { drawerContent = {
ModalNavigationDrawerContent( ModalNavigationDrawerContent(
selectedDestination = uiState.currentDestination, selectedDestination = currentRoute,
navigateToDestination = { navController.navigate(it.route) {
popUpTo(MainDestination.Search.route)
launchSingleTop = true
} },
navigationContentPosition = navigationContentPosition, navigationContentPosition = navigationContentPosition,
navigateToDestination = navigateToDestination,
onDrawerClicked = { onDrawerClicked = {
coroutineScope.launch { coroutineScope.launch {
drawerState.close() drawerState.close()
@@ -158,9 +184,8 @@ private fun MainNavigationWrapper(
navigationType = navigationType, navigationType = navigationType,
contentType = contentType, contentType = contentType,
displayFeatures = displayFeatures, displayFeatures = displayFeatures,
navigationContentPosition = navigationContentPosition,
uiState = uiState, uiState = uiState,
navigateToDestination = navigateToDestination, navController = navController,
onDrawerClicked = openDrawer, onDrawerClicked = openDrawer,
closeDetailScreen = closeDetailScreen, closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange, onQueryChange = onQueryChange,
@@ -170,25 +195,43 @@ private fun MainNavigationWrapper(
} }
} }
@Composable
fun NotImplemented() {
Box(Modifier.fillMaxSize()) {
Column(
Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("( ⁄•⁄ω⁄•⁄ )", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f))
Text(stringResource(R.string.not_implemented), textAlign = TextAlign.Center)
}
}
}
@Composable @Composable
fun MainContent( fun MainContent(
navigationType: NavigationType, navigationType: NavigationType,
contentType: ContentType, contentType: ContentType,
displayFeatures: List<DisplayFeature>, displayFeatures: List<DisplayFeature>,
navigationContentPosition: NavigationContentPosition, uiState: SearchState,
uiState: MainUIState, navController: NavHostController,
navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit, onDrawerClicked: () -> Unit,
closeDetailScreen: () -> Unit, closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit, onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit loadSearchResult: (IntRange) -> Unit
) { ) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(visible = navigationType == NavigationType.NAVIGATION_RAIL) { AnimatedVisibility(visible = navigationType == NavigationType.NAVIGATION_RAIL) {
MainNavigationRail( MainNavigationRail(
selectedDestination = uiState.currentDestination, selectedDestination = currentRoute,
navigationContentPosition = navigationContentPosition, navigateToDestination = { navController.navigate(it.route) {
navigateToDestination = navigateToDestination, popUpTo(MainDestination.Search.route)
launchSingleTop = true
} },
onDrawerClicked = onDrawerClicked onDrawerClicked = onDrawerClicked
) )
} }
@@ -198,27 +241,55 @@ fun MainContent(
.background(MaterialTheme.colorScheme.inverseOnSurface) .background(MaterialTheme.colorScheme.inverseOnSurface)
) { ) {
Box( Box(
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.run { .run {
if (navigationType == NavigationType.BOTTOM_NAVIGATION) { if (navigationType == NavigationType.BOTTOM_NAVIGATION) {
this.consumeWindowInsets(WindowInsets.ime) this
.consumeWindowInsets(WindowInsets.ime)
.consumeWindowInsets(WindowInsets.navigationBars) .consumeWindowInsets(WindowInsets.navigationBars)
} else this } else this
} }
) { ) {
MainScreen( NavHost(
contentType = contentType, modifier = Modifier.fillMaxSize(),
displayFeatures = displayFeatures, navController = navController,
uiState = uiState, startDestination = MainDestination.Search.route
closeDetailScreen = closeDetailScreen, ) {
onQueryChange = onQueryChange, composable(MainDestination.Search.route) {
loadSearchResult = loadSearchResult SearchScreen(
) contentType = contentType,
displayFeatures = displayFeatures,
uiState = uiState,
closeDetailScreen = closeDetailScreen,
onQueryChange = onQueryChange,
loadSearchResult = loadSearchResult
)
}
composable(MainDestination.History.route) {
NotImplemented()
}
composable(MainDestination.Downloads.route) {
NotImplemented()
}
composable(MainDestination.Favorites.route) {
NotImplemented()
}
activity(MainDestination.Settings.route) {
activityClass = SettingsActivity::class
}
activity(MainDestination.ImageViewer.route) {
activityClass = ReaderActivity::class
}
}
} }
AnimatedVisibility(visible = navigationType == NavigationType.BOTTOM_NAVIGATION) { AnimatedVisibility(visible = navigationType == NavigationType.BOTTOM_NAVIGATION) {
BottomNavigationBar( BottomNavigationBar(
selectedDestination = uiState.currentDestination, selectedDestination = currentRoute,
navigateToDestination = navigateToDestination navigateToDestination = { navController.navigate(it.route) {
popUpTo(MainDestination.Search.route)
launchSingleTop = true
} }
) )
} }
} }

View File

@@ -1,10 +1,13 @@
package xyz.quaver.pupil.ui.composable package xyz.quaver.pupil.ui.composable
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -14,34 +17,48 @@ sealed interface MainDestination {
val icon: ImageVector val icon: ImageVector
val textId: Int val textId: Int
object Search: MainDestination { data object Search: MainDestination {
override val route = "search" override val route = "search"
override val icon = Icons.Default.Search override val icon = Icons.Default.Search
override val textId = R.string.main_destination_search override val textId = R.string.main_destination_search
} }
object History: MainDestination { data object History: MainDestination {
override val route = "history" override val route = "history"
override val icon = Icons.Default.History override val icon = Icons.Default.History
override val textId = R.string.main_destination_history override val textId = R.string.main_destination_history
} }
object Downloads: MainDestination { data object Downloads: MainDestination {
override val route = "downloads" override val route = "downloads"
override val icon = Icons.Default.Download override val icon = Icons.Default.Download
override val textId = R.string.main_destination_downloads override val textId = R.string.main_destination_downloads
} }
object Favorites: MainDestination { data object Favorites: MainDestination {
override val route = "favorites" override val route = "favorites"
override val icon = Icons.Default.Favorite override val icon = Icons.Default.Favorite
override val textId = R.string.main_destination_favorites override val textId = R.string.main_destination_favorites
} }
data object Settings: MainDestination {
override val route = "settings"
override val icon = Icons.Default.Settings
override val textId = R.string.main_destination_settings
}
data object ImageViewer: MainDestination {
override val route = "image_viewer"
override val icon = Icons.AutoMirrored.Filled.MenuBook
override val textId = R.string.main_destination_image_viewer
}
} }
val mainDestinations = listOf( val mainDestinations = listOf(
MainDestination.Search, MainDestination.Search,
MainDestination.History, MainDestination.History,
MainDestination.Downloads, MainDestination.Downloads,
MainDestination.Favorites MainDestination.Favorites,
MainDestination.Settings
) )

View File

@@ -43,13 +43,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset import androidx.compose.ui.unit.offset
import androidx.navigation.NavDestination
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@Composable @Composable
fun PermanentNavigationDrawerContent( fun PermanentNavigationDrawerContent(
selectedDestination: MainDestination, selectedDestination: String?,
navigateToDestination: (MainDestination) -> Unit,
navigationContentPosition: NavigationContentPosition, navigationContentPosition: NavigationContentPosition,
navigateToDestination: (MainDestination) -> Unit
) { ) {
PermanentDrawerSheet( PermanentDrawerSheet(
modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp), modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp),
@@ -97,7 +98,7 @@ fun PermanentNavigationDrawerContent(
contentDescription = stringResource(destination.textId) contentDescription = stringResource(destination.textId)
) )
}, },
selected = selectedDestination.route == destination.route, selected = selectedDestination == destination.route,
colors = NavigationDrawerItemDefaults.colors( colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = Color.Transparent unselectedContainerColor = Color.Transparent
), ),
@@ -113,7 +114,7 @@ fun PermanentNavigationDrawerContent(
@Composable @Composable
fun ModalNavigationDrawerContent( fun ModalNavigationDrawerContent(
selectedDestination: MainDestination, selectedDestination: String?,
navigationContentPosition: NavigationContentPosition, navigationContentPosition: NavigationContentPosition,
navigateToDestination: (MainDestination) -> Unit, navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit onDrawerClicked: () -> Unit
@@ -177,7 +178,7 @@ fun ModalNavigationDrawerContent(
contentDescription = stringResource(destination.textId) contentDescription = stringResource(destination.textId)
) )
}, },
selected = selectedDestination.route == destination.route, selected = selectedDestination == destination.route,
colors = NavigationDrawerItemDefaults.colors( colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = Color.Transparent unselectedContainerColor = Color.Transparent
), ),
@@ -193,8 +194,7 @@ fun ModalNavigationDrawerContent(
@Composable @Composable
fun MainNavigationRail( fun MainNavigationRail(
selectedDestination: MainDestination, selectedDestination: String?,
navigationContentPosition: NavigationContentPosition,
navigateToDestination: (MainDestination) -> Unit, navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit onDrawerClicked: () -> Unit
) { ) {
@@ -220,7 +220,7 @@ fun MainNavigationRail(
) { ) {
mainDestinations.forEach { destination -> mainDestinations.forEach { destination ->
NavigationRailItem( NavigationRailItem(
selected = selectedDestination.route == destination.route, selected = selectedDestination == destination.route,
onClick = { navigateToDestination(destination) }, onClick = { navigateToDestination(destination) },
icon = { icon = {
Icon( Icon(
@@ -236,13 +236,13 @@ fun MainNavigationRail(
@Composable @Composable
fun BottomNavigationBar( fun BottomNavigationBar(
selectedDestination: MainDestination, selectedDestination: String?,
navigateToDestination: (MainDestination) -> Unit navigateToDestination: (MainDestination) -> Unit
) { ) {
NavigationBar(modifier = Modifier.fillMaxWidth(), windowInsets = WindowInsets.ime.union(WindowInsets.navigationBars)) { NavigationBar(modifier = Modifier.fillMaxWidth(), windowInsets = WindowInsets.ime.union(WindowInsets.navigationBars)) {
mainDestinations.forEach { destination -> mainDestinations.forEach { destination ->
NavigationBarItem( NavigationBarItem(
selected = selectedDestination.route == destination.route, selected = selectedDestination == destination.route,
onClick = { navigateToDestination(destination) }, onClick = { navigateToDestination(destination) },
icon = { icon = {
Icon( Icon(

View File

@@ -96,7 +96,7 @@ import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.theme.Blue600 import xyz.quaver.pupil.ui.theme.Blue600
import xyz.quaver.pupil.ui.theme.Pink600 import xyz.quaver.pupil.ui.theme.Pink600
import xyz.quaver.pupil.ui.theme.Yellow400 import xyz.quaver.pupil.ui.theme.Yellow400
import xyz.quaver.pupil.ui.viewmodel.MainUIState import xyz.quaver.pupil.ui.viewmodel.SearchState
import kotlin.math.roundToInt import kotlin.math.roundToInt
private val iconMap = mapOf( private val iconMap = mapOf(
@@ -304,7 +304,6 @@ fun SearchBar(
query: SearchQuery?, query: SearchQuery?,
onQueryChange: (SearchQuery?) -> Unit, onQueryChange: (SearchQuery?) -> Unit,
onSearchBarPositioned: (Int) -> Unit, onSearchBarPositioned: (Int) -> Unit,
onSearch: () -> Unit,
topOffset: Int, topOffset: Int,
onTopOffsetChange: (Int) -> Unit, onTopOffsetChange: (Int) -> Unit,
content: @Composable () -> Unit content: @Composable () -> Unit
@@ -404,85 +403,6 @@ fun SearchBar(
} }
} }
@Composable
fun MainScreen(
contentType: ContentType,
displayFeatures: List<DisplayFeature>,
uiState: MainUIState,
closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit
) {
LaunchedEffect(contentType) {
if (contentType == ContentType.SINGLE_PANE && !uiState.isDetailOnlyOpen) {
closeDetailScreen()
}
}
val galleryLazyListState = rememberLazyListState()
val itemsPerPage by remember { mutableIntStateOf(20) }
val pageToRange: (Int) -> IntRange = remember(itemsPerPage) {{ page ->
page * itemsPerPage ..< (page+1) * itemsPerPage
}}
val currentPage = remember(uiState) {
if (uiState.currentRange != IntRange.EMPTY) {
uiState.currentRange.first / itemsPerPage
} else 0
}
val maxPage = remember(itemsPerPage, uiState) {
if (uiState.galleryCount != null) {
uiState.galleryCount / itemsPerPage + if (uiState.galleryCount % itemsPerPage != 0) 1 else 0
} else 0
}
val loadResult: (Int) -> Unit = remember(loadSearchResult) {{ page ->
loadSearchResult(pageToRange(page))
}}
LaunchedEffect(uiState.query, uiState.currentDestination) { loadSearchResult(pageToRange(0)) }
if (contentType == ContentType.DUAL_PANE) {
TwoPane(
first = {
GalleryList(
contentType = contentType,
galleries = uiState.galleries,
query = uiState.query,
currentPage = currentPage,
maxPage = maxPage,
loading = uiState.loading,
error = uiState.error,
galleryLazyListState = galleryLazyListState,
onQueryChange = onQueryChange,
onPageChange = loadResult
)
},
second = {
},
strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f, gapWidth = 16.dp),
displayFeatures = displayFeatures
)
} else {
GalleryList(
contentType = contentType,
galleries = uiState.galleries,
query = uiState.query,
currentPage = currentPage,
maxPage = maxPage,
loading = uiState.loading,
error = uiState.error,
galleryLazyListState = galleryLazyListState,
onQueryChange = onQueryChange,
onPageChange = loadResult
)
}
}
@Composable @Composable
fun GalleryList( fun GalleryList(
contentType: ContentType, contentType: ContentType,
@@ -492,14 +412,8 @@ fun GalleryList(
maxPage: Int, maxPage: Int,
loading: Boolean = false, loading: Boolean = false,
error: Boolean = false, error: Boolean = false,
openedGallery: GalleryInfo? = null,
onPageChange: (Int) -> Unit, onPageChange: (Int) -> Unit,
onQueryChange: (SearchQuery?) -> Unit = {}, onQueryChange: (SearchQuery?) -> Unit = {},
search: () -> Unit = {},
selectedGalleryIds: Set<Int> = emptySet(),
toggleGallerySelection: (Int) -> Unit = {},
galleryLazyListState: LazyListState,
navigateToDetails: (GalleryInfo, ContentType) -> Unit = { gi, ct -> }
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
var topOffset by remember { mutableIntStateOf(0) } var topOffset by remember { mutableIntStateOf(0) }
@@ -522,7 +436,6 @@ fun GalleryList(
query = query, query = query,
onQueryChange = onQueryChange, onQueryChange = onQueryChange,
onSearchBarPositioned = { searchBarPosition = it }, onSearchBarPositioned = { searchBarPosition = it },
onSearch = search,
topOffset = topOffset, topOffset = topOffset,
onTopOffsetChange = { topOffset = it }, onTopOffsetChange = { topOffset = it },
) { ) {
@@ -563,7 +476,7 @@ fun GalleryList(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
state = listState state = listState
) { ) {
items(galleries) {galleryInfo -> items(galleries, key = { it.id }) {galleryInfo ->
DetailedGalleryInfo( DetailedGalleryInfo(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -576,3 +489,79 @@ fun GalleryList(
} }
} }
} }
@Composable
fun SearchScreen(
contentType: ContentType,
displayFeatures: List<DisplayFeature>,
uiState: SearchState,
closeDetailScreen: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit
) {
LaunchedEffect(contentType) {
if (contentType == ContentType.SINGLE_PANE && !uiState.isDetailOnlyOpen) {
closeDetailScreen()
}
}
val itemsPerPage by remember { mutableIntStateOf(20) }
val pageToRange: (Int) -> IntRange = remember(itemsPerPage) {{ page ->
page * itemsPerPage ..< (page+1) * itemsPerPage
}}
val currentPage = remember(uiState) {
if (uiState.currentRange != IntRange.EMPTY) {
uiState.currentRange.first / itemsPerPage
} else 0
}
val maxPage = remember(itemsPerPage, uiState) {
if (uiState.galleryCount != null) {
uiState.galleryCount / itemsPerPage + if (uiState.galleryCount % itemsPerPage != 0) 1 else 0
} else 0
}
val loadResult: (Int) -> Unit = remember(loadSearchResult) {{ page ->
loadSearchResult(pageToRange(page))
}}
LaunchedEffect(uiState.query, currentPage) { loadSearchResult(pageToRange(currentPage)) }
if (contentType == ContentType.DUAL_PANE) {
TwoPane(
first = {
GalleryList(
contentType = contentType,
galleries = uiState.galleries,
query = uiState.query,
currentPage = currentPage,
maxPage = maxPage,
loading = uiState.loading,
error = uiState.error,
onQueryChange = onQueryChange,
onPageChange = loadResult
)
},
second = {
},
strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f, gapWidth = 16.dp),
displayFeatures = displayFeatures
)
} else {
GalleryList(
contentType = contentType,
galleries = uiState.galleries,
query = uiState.query,
currentPage = currentPage,
maxPage = maxPage,
loading = uiState.loading,
error = uiState.error,
onQueryChange = onQueryChange,
onPageChange = loadResult
)
}
}

View File

@@ -9,23 +9,15 @@ import kotlinx.coroutines.launch
import xyz.quaver.pupil.networking.GalleryInfo import xyz.quaver.pupil.networking.GalleryInfo
import xyz.quaver.pupil.networking.GallerySearchSource import xyz.quaver.pupil.networking.GallerySearchSource
import xyz.quaver.pupil.networking.SearchQuery import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.composable.MainDestination
import xyz.quaver.pupil.ui.composable.mainDestinations
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class MainViewModel : ViewModel() { class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(MainUIState()) private val _uiState = MutableStateFlow(SearchState())
val uiState: StateFlow<MainUIState> = _uiState val searchState: StateFlow<SearchState> = _uiState
private var searchSource: GallerySearchSource = GallerySearchSource(null) private var searchSource: GallerySearchSource = GallerySearchSource(null)
private var job: Job? = null private var job: Job? = null
fun navigateToDestination(destination: MainDestination) {
_uiState.value = MainUIState(
currentDestination = destination
)
}
fun closeDetailScreen() { fun closeDetailScreen() {
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
isDetailOnlyOpen = false isDetailOnlyOpen = false
@@ -45,7 +37,7 @@ class MainViewModel : ViewModel() {
fun loadSearchResult(range: IntRange) { fun loadSearchResult(range: IntRange) {
job?.cancel() job?.cancel()
job = viewModelScope.launch { job = viewModelScope.launch {
val sanitizedRange = max(range.first, 0) .. min(range.last, uiState.value.galleryCount ?: Int.MAX_VALUE) val sanitizedRange = max(range.first, 0) .. min(range.last, searchState.value.galleryCount ?: Int.MAX_VALUE)
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
loading = true, loading = true,
currentRange = sanitizedRange currentRange = sanitizedRange
@@ -72,8 +64,7 @@ class MainViewModel : ViewModel() {
} }
} }
data class MainUIState( data class SearchState(
val currentDestination: MainDestination = mainDestinations.first(),
val query: SearchQuery? = null, val query: SearchQuery? = null,
val galleries: List<GalleryInfo> = emptyList(), val galleries: List<GalleryInfo> = emptyList(),
val loading: Boolean = false, val loading: Boolean = false,

View File

@@ -33,6 +33,7 @@
<string name="default_query_dialog_filter_guro">グロフィルター</string> <string name="default_query_dialog_filter_guro">グロフィルター</string>
<string name="default_query_dialog_language">"言語: "</string> <string name="default_query_dialog_language">"言語: "</string>
<string name="default_query_dialog_title">デフォルトキーワード設定</string> <string name="default_query_dialog_title">デフォルトキーワード設定</string>
<string name="main_destination_settings">設定</string>
<string name="main_open_navigation_drawer">メニューを開く</string> <string name="main_open_navigation_drawer">メニューを開く</string>
<string name="main_drawer_group_contact_title">お問い合わせ先</string> <string name="main_drawer_group_contact_title">お問い合わせ先</string>
<string name="main_drawer_group_contact_homepage">ホームページ</string> <string name="main_drawer_group_contact_homepage">ホームページ</string>
@@ -166,4 +167,6 @@
<string name="search_add_query_item_tag">タグ</string> <string name="search_add_query_item_tag">タグ</string>
<string name="move_to_page">%1$d ページへ移動</string> <string name="move_to_page">%1$d ページへ移動</string>
<string name="search_bar_edit_tag">タッチして編集</string> <string name="search_bar_edit_tag">タッチして編集</string>
<string name="main_destination_image_viewer">イメージビューア</string>
<string name="not_implemented">この機能はまだ実装されていません</string>
</resources> </resources>

View File

@@ -36,6 +36,8 @@
<string name="main_drawer_group_contact_github">Github</string> <string name="main_drawer_group_contact_github">Github</string>
<string name="main_drawer_group_contact_help">도움말</string> <string name="main_drawer_group_contact_help">도움말</string>
<string name="main_drawer_group_contact_homepage">홈페이지</string> <string name="main_drawer_group_contact_homepage">홈페이지</string>
<string name="main_destination_settings">설정</string>
<string name="main_destination_image_viewer">뷰어</string>
<string name="main_open_navigation_drawer">메뉴 열기</string> <string name="main_open_navigation_drawer">메뉴 열기</string>
<string name="main_drawer_group_contact_title">문의</string> <string name="main_drawer_group_contact_title">문의</string>
<string name="reader_fab_fullscreen">전체 화면</string> <string name="reader_fab_fullscreen">전체 화면</string>
@@ -166,4 +168,5 @@
<string name="search_add_query_item_tag">태그</string> <string name="search_add_query_item_tag">태그</string>
<string name="move_to_page">%1$d 페이지로 이동</string> <string name="move_to_page">%1$d 페이지로 이동</string>
<string name="search_bar_edit_tag">터치하여 수정</string> <string name="search_bar_edit_tag">터치하여 수정</string>
<string name="not_implemented">이 기능은 아직 개발 중입니다</string>
</resources> </resources>

View File

@@ -32,6 +32,8 @@
<string name="unlimited">Unlimited</string> <string name="unlimited">Unlimited</string>
<string name="not_implemented">This feature is not implemented yet</string>
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
<string name="channel_download">Download</string> <string name="channel_download">Download</string>
@@ -57,6 +59,8 @@
<string name="main_destination_history">History</string> <string name="main_destination_history">History</string>
<string name="main_destination_downloads">Downloads</string> <string name="main_destination_downloads">Downloads</string>
<string name="main_destination_favorites">Favorites</string> <string name="main_destination_favorites">Favorites</string>
<string name="main_destination_settings">Settings</string>
<string name="main_destination_image_viewer">Reader</string>
<string name="main_open_navigation_drawer">Open Navigation Drawer</string> <string name="main_open_navigation_drawer">Open Navigation Drawer</string>
<string name="main_close_navigation_drawer">Close Navigation Drawer</string> <string name="main_close_navigation_drawer">Close Navigation Drawer</string>
<string name="main_drawer_group_contact_title">Contact</string> <string name="main_drawer_group_contact_title">Contact</string>