Basic Navigation
This commit is contained in:
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@@ -31,4 +31,5 @@
|
||||
-keepclasseswithmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class xyz.quaver.pupil.** { *; }
|
||||
-keep class xyz.quaver.pupil.** { *; }
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
@@ -12,7 +12,7 @@
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 69,
|
||||
"versionName": "5.3.10",
|
||||
"versionName": "6.0.0",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -27,10 +27,10 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.adaptive.calculateDisplayFeatures
|
||||
import xyz.quaver.pupil.ui.composable.MainApp
|
||||
import xyz.quaver.pupil.ui.theme.AppTheme
|
||||
import xyz.quaver.pupil.ui.viewmodel.MainUIState
|
||||
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
@@ -48,13 +48,15 @@ class MainActivity : BaseActivity() {
|
||||
val windowSize = calculateWindowSizeClass(this)
|
||||
val displayFeatures = calculateDisplayFeatures(this)
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.searchState.collectAsStateWithLifecycle()
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
MainApp(
|
||||
windowSize = windowSize,
|
||||
displayFeatures = displayFeatures,
|
||||
uiState = uiState,
|
||||
navigateToDestination = viewModel::navigateToDestination,
|
||||
navController = navController,
|
||||
closeDetailScreen = viewModel::closeDetailScreen,
|
||||
onQueryChange = viewModel::onQueryChange,
|
||||
loadSearchResult = viewModel::loadSearchResult
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package xyz.quaver.pupil.ui.composable
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -14,25 +18,39 @@ import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.PermanentNavigationDrawer
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
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.FoldingFeature
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.pupil.R
|
||||
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
|
||||
fun MainApp(
|
||||
windowSize: WindowSizeClass,
|
||||
displayFeatures: List<DisplayFeature>,
|
||||
uiState: MainUIState,
|
||||
navigateToDestination: (MainDestination) -> Unit,
|
||||
uiState: SearchState,
|
||||
navController: NavHostController,
|
||||
closeDetailScreen: () -> Unit,
|
||||
onQueryChange: (SearchQuery?) -> Unit,
|
||||
loadSearchResult: (IntRange) -> Unit
|
||||
@@ -87,7 +105,7 @@ fun MainApp(
|
||||
displayFeatures,
|
||||
navigationContentPosition,
|
||||
uiState,
|
||||
navigateToDestination,
|
||||
navController,
|
||||
closeDetailScreen = closeDetailScreen,
|
||||
onQueryChange = onQueryChange,
|
||||
loadSearchResult = loadSearchResult
|
||||
@@ -100,8 +118,8 @@ private fun MainNavigationWrapper(
|
||||
contentType: ContentType,
|
||||
displayFeatures: List<DisplayFeature>,
|
||||
navigationContentPosition: NavigationContentPosition,
|
||||
uiState: MainUIState,
|
||||
navigateToDestination: (MainDestination) -> Unit,
|
||||
uiState: SearchState,
|
||||
navController: NavHostController,
|
||||
closeDetailScreen: () -> Unit,
|
||||
onQueryChange: (SearchQuery?) -> Unit,
|
||||
loadSearchResult: (IntRange) -> Unit
|
||||
@@ -109,6 +127,9 @@ private fun MainNavigationWrapper(
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
val openDrawer: () -> Unit = {
|
||||
coroutineScope.launch {
|
||||
drawerState.open()
|
||||
@@ -119,9 +140,12 @@ private fun MainNavigationWrapper(
|
||||
PermanentNavigationDrawer(
|
||||
drawerContent = {
|
||||
PermanentNavigationDrawerContent(
|
||||
selectedDestination = uiState.currentDestination,
|
||||
selectedDestination = currentRoute,
|
||||
navigateToDestination = { navController.navigate(it.route) {
|
||||
popUpTo(MainDestination.Search.route)
|
||||
launchSingleTop = true
|
||||
} },
|
||||
navigationContentPosition = navigationContentPosition,
|
||||
navigateToDestination = navigateToDestination
|
||||
)
|
||||
}
|
||||
) {
|
||||
@@ -129,9 +153,8 @@ private fun MainNavigationWrapper(
|
||||
navigationType = navigationType,
|
||||
contentType = contentType,
|
||||
displayFeatures = displayFeatures,
|
||||
navigationContentPosition = navigationContentPosition,
|
||||
uiState = uiState,
|
||||
navigateToDestination = navigateToDestination,
|
||||
navController = navController,
|
||||
onDrawerClicked = openDrawer,
|
||||
closeDetailScreen = closeDetailScreen,
|
||||
onQueryChange = onQueryChange,
|
||||
@@ -142,9 +165,12 @@ private fun MainNavigationWrapper(
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
ModalNavigationDrawerContent(
|
||||
selectedDestination = uiState.currentDestination,
|
||||
selectedDestination = currentRoute,
|
||||
navigateToDestination = { navController.navigate(it.route) {
|
||||
popUpTo(MainDestination.Search.route)
|
||||
launchSingleTop = true
|
||||
} },
|
||||
navigationContentPosition = navigationContentPosition,
|
||||
navigateToDestination = navigateToDestination,
|
||||
onDrawerClicked = {
|
||||
coroutineScope.launch {
|
||||
drawerState.close()
|
||||
@@ -158,9 +184,8 @@ private fun MainNavigationWrapper(
|
||||
navigationType = navigationType,
|
||||
contentType = contentType,
|
||||
displayFeatures = displayFeatures,
|
||||
navigationContentPosition = navigationContentPosition,
|
||||
uiState = uiState,
|
||||
navigateToDestination = navigateToDestination,
|
||||
navController = navController,
|
||||
onDrawerClicked = openDrawer,
|
||||
closeDetailScreen = closeDetailScreen,
|
||||
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
|
||||
fun MainContent(
|
||||
navigationType: NavigationType,
|
||||
contentType: ContentType,
|
||||
displayFeatures: List<DisplayFeature>,
|
||||
navigationContentPosition: NavigationContentPosition,
|
||||
uiState: MainUIState,
|
||||
navigateToDestination: (MainDestination) -> Unit,
|
||||
uiState: SearchState,
|
||||
navController: NavHostController,
|
||||
onDrawerClicked: () -> Unit,
|
||||
closeDetailScreen: () -> Unit,
|
||||
onQueryChange: (SearchQuery?) -> Unit,
|
||||
loadSearchResult: (IntRange) -> Unit
|
||||
) {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
AnimatedVisibility(visible = navigationType == NavigationType.NAVIGATION_RAIL) {
|
||||
MainNavigationRail(
|
||||
selectedDestination = uiState.currentDestination,
|
||||
navigationContentPosition = navigationContentPosition,
|
||||
navigateToDestination = navigateToDestination,
|
||||
selectedDestination = currentRoute,
|
||||
navigateToDestination = { navController.navigate(it.route) {
|
||||
popUpTo(MainDestination.Search.route)
|
||||
launchSingleTop = true
|
||||
} },
|
||||
onDrawerClicked = onDrawerClicked
|
||||
)
|
||||
}
|
||||
@@ -198,27 +241,55 @@ fun MainContent(
|
||||
.background(MaterialTheme.colorScheme.inverseOnSurface)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.run {
|
||||
if (navigationType == NavigationType.BOTTOM_NAVIGATION) {
|
||||
this.consumeWindowInsets(WindowInsets.ime)
|
||||
this
|
||||
.consumeWindowInsets(WindowInsets.ime)
|
||||
.consumeWindowInsets(WindowInsets.navigationBars)
|
||||
} else this
|
||||
}
|
||||
) {
|
||||
MainScreen(
|
||||
contentType = contentType,
|
||||
displayFeatures = displayFeatures,
|
||||
uiState = uiState,
|
||||
closeDetailScreen = closeDetailScreen,
|
||||
onQueryChange = onQueryChange,
|
||||
loadSearchResult = loadSearchResult
|
||||
)
|
||||
NavHost(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
navController = navController,
|
||||
startDestination = MainDestination.Search.route
|
||||
) {
|
||||
composable(MainDestination.Search.route) {
|
||||
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) {
|
||||
BottomNavigationBar(
|
||||
selectedDestination = uiState.currentDestination,
|
||||
navigateToDestination = navigateToDestination
|
||||
selectedDestination = currentRoute,
|
||||
navigateToDestination = { navController.navigate(it.route) {
|
||||
popUpTo(MainDestination.Search.route)
|
||||
launchSingleTop = true
|
||||
} }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package xyz.quaver.pupil.ui.composable
|
||||
|
||||
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.Favorite
|
||||
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.Settings
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import xyz.quaver.pupil.R
|
||||
@@ -14,34 +17,48 @@ sealed interface MainDestination {
|
||||
val icon: ImageVector
|
||||
val textId: Int
|
||||
|
||||
object Search: MainDestination {
|
||||
data object Search: MainDestination {
|
||||
override val route = "search"
|
||||
override val icon = Icons.Default.Search
|
||||
override val textId = R.string.main_destination_search
|
||||
}
|
||||
|
||||
object History: MainDestination {
|
||||
data object History: MainDestination {
|
||||
override val route = "history"
|
||||
override val icon = Icons.Default.History
|
||||
override val textId = R.string.main_destination_history
|
||||
}
|
||||
|
||||
object Downloads: MainDestination {
|
||||
data object Downloads: MainDestination {
|
||||
override val route = "downloads"
|
||||
override val icon = Icons.Default.Download
|
||||
override val textId = R.string.main_destination_downloads
|
||||
}
|
||||
|
||||
object Favorites: MainDestination {
|
||||
data object Favorites: MainDestination {
|
||||
override val route = "favorites"
|
||||
override val icon = Icons.Default.Favorite
|
||||
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(
|
||||
MainDestination.Search,
|
||||
MainDestination.History,
|
||||
MainDestination.Downloads,
|
||||
MainDestination.Favorites
|
||||
MainDestination.Favorites,
|
||||
MainDestination.Settings
|
||||
)
|
||||
@@ -43,13 +43,14 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import androidx.navigation.NavDestination
|
||||
import xyz.quaver.pupil.R
|
||||
|
||||
@Composable
|
||||
fun PermanentNavigationDrawerContent(
|
||||
selectedDestination: MainDestination,
|
||||
selectedDestination: String?,
|
||||
navigateToDestination: (MainDestination) -> Unit,
|
||||
navigationContentPosition: NavigationContentPosition,
|
||||
navigateToDestination: (MainDestination) -> Unit
|
||||
) {
|
||||
PermanentDrawerSheet(
|
||||
modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp),
|
||||
@@ -97,7 +98,7 @@ fun PermanentNavigationDrawerContent(
|
||||
contentDescription = stringResource(destination.textId)
|
||||
)
|
||||
},
|
||||
selected = selectedDestination.route == destination.route,
|
||||
selected = selectedDestination == destination.route,
|
||||
colors = NavigationDrawerItemDefaults.colors(
|
||||
unselectedContainerColor = Color.Transparent
|
||||
),
|
||||
@@ -113,7 +114,7 @@ fun PermanentNavigationDrawerContent(
|
||||
|
||||
@Composable
|
||||
fun ModalNavigationDrawerContent(
|
||||
selectedDestination: MainDestination,
|
||||
selectedDestination: String?,
|
||||
navigationContentPosition: NavigationContentPosition,
|
||||
navigateToDestination: (MainDestination) -> Unit,
|
||||
onDrawerClicked: () -> Unit
|
||||
@@ -177,7 +178,7 @@ fun ModalNavigationDrawerContent(
|
||||
contentDescription = stringResource(destination.textId)
|
||||
)
|
||||
},
|
||||
selected = selectedDestination.route == destination.route,
|
||||
selected = selectedDestination == destination.route,
|
||||
colors = NavigationDrawerItemDefaults.colors(
|
||||
unselectedContainerColor = Color.Transparent
|
||||
),
|
||||
@@ -193,8 +194,7 @@ fun ModalNavigationDrawerContent(
|
||||
|
||||
@Composable
|
||||
fun MainNavigationRail(
|
||||
selectedDestination: MainDestination,
|
||||
navigationContentPosition: NavigationContentPosition,
|
||||
selectedDestination: String?,
|
||||
navigateToDestination: (MainDestination) -> Unit,
|
||||
onDrawerClicked: () -> Unit
|
||||
) {
|
||||
@@ -220,7 +220,7 @@ fun MainNavigationRail(
|
||||
) {
|
||||
mainDestinations.forEach { destination ->
|
||||
NavigationRailItem(
|
||||
selected = selectedDestination.route == destination.route,
|
||||
selected = selectedDestination == destination.route,
|
||||
onClick = { navigateToDestination(destination) },
|
||||
icon = {
|
||||
Icon(
|
||||
@@ -236,13 +236,13 @@ fun MainNavigationRail(
|
||||
|
||||
@Composable
|
||||
fun BottomNavigationBar(
|
||||
selectedDestination: MainDestination,
|
||||
selectedDestination: String?,
|
||||
navigateToDestination: (MainDestination) -> Unit
|
||||
) {
|
||||
NavigationBar(modifier = Modifier.fillMaxWidth(), windowInsets = WindowInsets.ime.union(WindowInsets.navigationBars)) {
|
||||
mainDestinations.forEach { destination ->
|
||||
NavigationBarItem(
|
||||
selected = selectedDestination.route == destination.route,
|
||||
selected = selectedDestination == destination.route,
|
||||
onClick = { navigateToDestination(destination) },
|
||||
icon = {
|
||||
Icon(
|
||||
|
||||
@@ -96,7 +96,7 @@ import xyz.quaver.pupil.networking.SearchQuery
|
||||
import xyz.quaver.pupil.ui.theme.Blue600
|
||||
import xyz.quaver.pupil.ui.theme.Pink600
|
||||
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
|
||||
|
||||
private val iconMap = mapOf(
|
||||
@@ -304,7 +304,6 @@ fun SearchBar(
|
||||
query: SearchQuery?,
|
||||
onQueryChange: (SearchQuery?) -> Unit,
|
||||
onSearchBarPositioned: (Int) -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
topOffset: Int,
|
||||
onTopOffsetChange: (Int) -> 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
|
||||
fun GalleryList(
|
||||
contentType: ContentType,
|
||||
@@ -492,14 +412,8 @@ fun GalleryList(
|
||||
maxPage: Int,
|
||||
loading: Boolean = false,
|
||||
error: Boolean = false,
|
||||
openedGallery: GalleryInfo? = null,
|
||||
onPageChange: (Int) -> Unit,
|
||||
onQueryChange: (SearchQuery?) -> Unit = {},
|
||||
search: () -> Unit = {},
|
||||
selectedGalleryIds: Set<Int> = emptySet(),
|
||||
toggleGallerySelection: (Int) -> Unit = {},
|
||||
galleryLazyListState: LazyListState,
|
||||
navigateToDetails: (GalleryInfo, ContentType) -> Unit = { gi, ct -> }
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
var topOffset by remember { mutableIntStateOf(0) }
|
||||
@@ -522,7 +436,6 @@ fun GalleryList(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearchBarPositioned = { searchBarPosition = it },
|
||||
onSearch = search,
|
||||
topOffset = topOffset,
|
||||
onTopOffsetChange = { topOffset = it },
|
||||
) {
|
||||
@@ -563,7 +476,7 @@ fun GalleryList(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
state = listState
|
||||
) {
|
||||
items(galleries) {galleryInfo ->
|
||||
items(galleries, key = { it.id }) {galleryInfo ->
|
||||
DetailedGalleryInfo(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -575,4 +488,80 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,15 @@ import kotlinx.coroutines.launch
|
||||
import xyz.quaver.pupil.networking.GalleryInfo
|
||||
import xyz.quaver.pupil.networking.GallerySearchSource
|
||||
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.min
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(MainUIState())
|
||||
val uiState: StateFlow<MainUIState> = _uiState
|
||||
private val _uiState = MutableStateFlow(SearchState())
|
||||
val searchState: StateFlow<SearchState> = _uiState
|
||||
private var searchSource: GallerySearchSource = GallerySearchSource(null)
|
||||
private var job: Job? = null
|
||||
|
||||
fun navigateToDestination(destination: MainDestination) {
|
||||
_uiState.value = MainUIState(
|
||||
currentDestination = destination
|
||||
)
|
||||
}
|
||||
|
||||
fun closeDetailScreen() {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isDetailOnlyOpen = false
|
||||
@@ -45,7 +37,7 @@ class MainViewModel : ViewModel() {
|
||||
fun loadSearchResult(range: IntRange) {
|
||||
job?.cancel()
|
||||
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(
|
||||
loading = true,
|
||||
currentRange = sanitizedRange
|
||||
@@ -72,8 +64,7 @@ class MainViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
data class MainUIState(
|
||||
val currentDestination: MainDestination = mainDestinations.first(),
|
||||
data class SearchState(
|
||||
val query: SearchQuery? = null,
|
||||
val galleries: List<GalleryInfo> = emptyList(),
|
||||
val loading: Boolean = false,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<string name="default_query_dialog_filter_guro">グロフィルター</string>
|
||||
<string name="default_query_dialog_language">"言語: "</string>
|
||||
<string name="default_query_dialog_title">デフォルトキーワード設定</string>
|
||||
<string name="main_destination_settings">設定</string>
|
||||
<string name="main_open_navigation_drawer">メニューを開く</string>
|
||||
<string name="main_drawer_group_contact_title">お問い合わせ先</string>
|
||||
<string name="main_drawer_group_contact_homepage">ホームページ</string>
|
||||
@@ -166,4 +167,6 @@
|
||||
<string name="search_add_query_item_tag">タグ</string>
|
||||
<string name="move_to_page">%1$d ページへ移動</string>
|
||||
<string name="search_bar_edit_tag">タッチして編集</string>
|
||||
<string name="main_destination_image_viewer">イメージビューア</string>
|
||||
<string name="not_implemented">この機能はまだ実装されていません</string>
|
||||
</resources>
|
||||
@@ -36,6 +36,8 @@
|
||||
<string name="main_drawer_group_contact_github">Github</string>
|
||||
<string name="main_drawer_group_contact_help">도움말</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_drawer_group_contact_title">문의</string>
|
||||
<string name="reader_fab_fullscreen">전체 화면</string>
|
||||
@@ -166,4 +168,5 @@
|
||||
<string name="search_add_query_item_tag">태그</string>
|
||||
<string name="move_to_page">%1$d 페이지로 이동</string>
|
||||
<string name="search_bar_edit_tag">터치하여 수정</string>
|
||||
<string name="not_implemented">이 기능은 아직 개발 중입니다</string>
|
||||
</resources>
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
<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="channel_download">Download</string>
|
||||
@@ -57,6 +59,8 @@
|
||||
<string name="main_destination_history">History</string>
|
||||
<string name="main_destination_downloads">Downloads</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_close_navigation_drawer">Close Navigation Drawer</string>
|
||||
<string name="main_drawer_group_contact_title">Contact</string>
|
||||
|
||||
Reference in New Issue
Block a user