Search bar skeleton

This commit is contained in:
tom5079
2024-02-24 10:24:36 -08:00
parent b0fedd78fb
commit 39b8bbc725
8 changed files with 331 additions and 22 deletions

View File

@@ -0,0 +1,84 @@
package xyz.quaver.pupil.networking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
interface TagLike {
fun toTag(): SearchQuery.Tag
}
@Serializable
data class Artist(val artist: String): TagLike {
override fun toTag() = SearchQuery.Tag("artist", artist)
}
@Serializable
data class Group(val group: String): TagLike {
override fun toTag() = SearchQuery.Tag("group", group)
}
@Serializable
data class Series(val series: String): TagLike {
override fun toTag() = SearchQuery.Tag("series", series)
}
@Serializable
data class Character(val character: String): TagLike {
override fun toTag() = SearchQuery.Tag("character", character)
}
@Serializable
data class GalleryTag(
val tag: String,
val female: String? = null,
val male: String? = null
): TagLike {
override fun toTag() = SearchQuery.Tag(
if (female.isNullOrEmpty() && male.isNullOrEmpty()) {
"tag"
} else if (male.isNullOrEmpty()) {
"female"
} else {
"male"
},
tag
)
}
@Serializable
data class Language(
@SerialName("galleryid") val galleryID: String,
val url: String,
@SerialName("language_localname") val localLanguageName: String,
val name: String
)
@Serializable
data class GalleryFiles(
@SerialName("haswebp") val hasWebP: Int = 0,
@SerialName("hasavif") val hasAVIF: Int = 0,
@SerialName("hasjxl") val hasJXL: Int = 0,
val height: Int,
val width: Int,
val hash: String,
val name: String,
)
@Serializable
data class GalleryInfo(
val id: String,
val title: String,
@SerialName("japanese_title") val japaneseTitle: String? = null,
val language: String? = null,
val type: String,
val date: String,
val artists: List<Artist>? = null,
val groups: List<Group>? = null,
@SerialName("parodys") val series: List<Series>? = null,
val tags: List<GalleryTag>? = null,
val related: List<Int> = emptyList(),
val languages: List<Language> = emptyList(),
val characters: List<Character>? = null,
@SerialName("scene_indexes") val sceneIndices: List<Int>? = emptyList(),
val files: List<GalleryFiles> = emptyList()
)

View File

@@ -1,6 +1,5 @@
package xyz.quaver.pupil.networking
import androidx.collection.mutableIntSetOf
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
@@ -12,6 +11,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import xyz.quaver.pupil.hitomi.max_node_size
import java.nio.ByteBuffer
import java.nio.IntBuffer

View File

@@ -4,7 +4,7 @@ sealed interface SearchQuery {
data class Tag(
val namespace: String?,
val tag: String
): SearchQuery {
): SearchQuery, TagLike {
companion object {
fun parseTag(tag: String): Tag {
val splitTag = tag.split(':', limit = 1)
@@ -18,6 +18,8 @@ sealed interface SearchQuery {
}
override fun toString() = if (namespace == null) tag else "$namespace:$tag"
override fun toTag() = this
}

View File

@@ -50,7 +50,8 @@ class MainActivity : BaseActivity() {
windowSize = windowSize,
displayFeatures = displayFeatures,
uiState = uiState,
navigateToDestination = viewModel::navigateToDestination
navigateToDestination = viewModel::navigateToDestination,
closeDetailScreen = viewModel::closeDetailScreen
)
}
}

View File

@@ -1,10 +0,0 @@
package xyz.quaver.pupil.ui.composable
import androidx.compose.runtime.Composable
@Composable
fun GalleryList(
) {
}

View File

@@ -27,7 +27,8 @@ fun MainApp(
windowSize: WindowSizeClass,
displayFeatures: List<DisplayFeature>,
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit
navigateToDestination: (MainDestination) -> Unit,
closeDetailScreen: () -> Unit
) {
val navigationType: NavigationType
val contentType: ContentType
@@ -79,9 +80,9 @@ fun MainApp(
displayFeatures,
navigationContentPosition,
uiState,
navigateToDestination
navigateToDestination,
closeDetailScreen = closeDetailScreen
)
}
@Composable
@@ -91,7 +92,8 @@ private fun MainNavigationWrapper(
displayFeatures: List<DisplayFeature>,
navigationContentPosition: NavigationContentPosition,
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit
navigateToDestination: (MainDestination) -> Unit,
closeDetailScreen: () -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
@@ -117,7 +119,8 @@ private fun MainNavigationWrapper(
navigationContentPosition = navigationContentPosition,
uiState = uiState,
navigateToDestination = navigateToDestination,
onDrawerClicked = openDrawer
onDrawerClicked = openDrawer,
closeDetailScreen = closeDetailScreen
)
}
} else {
@@ -143,7 +146,8 @@ private fun MainNavigationWrapper(
navigationContentPosition = navigationContentPosition,
uiState = uiState,
navigateToDestination = navigateToDestination,
onDrawerClicked = openDrawer
onDrawerClicked = openDrawer,
closeDetailScreen = closeDetailScreen
)
}
}
@@ -157,7 +161,8 @@ fun MainContent(
navigationContentPosition: NavigationContentPosition,
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit
onDrawerClicked: () -> Unit,
closeDetailScreen: () -> Unit
) {
Row(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(visible = navigationType == NavigationType.NAVIGATION_RAIL) {
@@ -173,7 +178,16 @@ fun MainContent(
.fillMaxSize()
.background(MaterialTheme.colorScheme.inverseOnSurface)
) {
Box(modifier = Modifier.weight(1f))
Box(
modifier = Modifier.weight(1f)
) {
MainScreen(
contentType = contentType,
displayFeatures = displayFeatures,
uiState = uiState,
closeDetailScreen = closeDetailScreen
)
}
AnimatedVisibility(visible = navigationType == NavigationType.BOTTOM_NAVIGATION) {
BottomNavigationBar(
selectedDestination = uiState.currentDestination,

View File

@@ -0,0 +1,204 @@
package xyz.quaver.pupil.ui.composable
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.layout.DisplayFeature
import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy
import com.google.accompanist.adaptive.TwoPane
import xyz.quaver.pupil.R
import xyz.quaver.pupil.networking.GalleryInfo
import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.viewmodel.MainUIState
@Composable
fun SearchBar(
contentType: ContentType,
query: SearchQuery?,
onQueryChange: (SearchQuery) -> Unit,
onSearch: () -> Unit,
topOffset: Int,
onTopOffsetChange: (Int) -> Unit,
content: @Composable () -> Unit
) {
var focused by remember { mutableStateOf(false) }
val scrimAlpha: Float by animateFloatAsState(if (focused && contentType == ContentType.SINGLE_PANE) 0.3f else 0f, label = "skrim alpha")
val interactionSource = remember { MutableInteractionSource() }
if (focused) {
BackHandler {
focused = false
}
}
LaunchedEffect(query) {
focused = false
}
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = scrimAlpha))
.clickable(
interactionSource = interactionSource,
indication = null
) {
focused = false
}
.safeDrawingPadding()
.padding(16.dp)
) {
val height: Dp by animateDpAsState(if (focused) maxHeight else 60.dp, label = "searchbar height")
Card(
modifier = Modifier
.fillMaxWidth()
.height(height)
.clickable(
interactionSource = interactionSource,
indication = null
) {
focused = true
},
shape = RoundedCornerShape(30.dp),
elevation = if (focused) CardDefaults.cardElevation(
defaultElevation = 4.dp
) else CardDefaults.cardElevation()
) {
Box {
androidx.compose.animation.AnimatedVisibility(query == null && !focused, enter = fadeIn(), exit = fadeOut()) {
Text(
modifier = Modifier
.height(60.dp)
.wrapContentHeight()
.padding(horizontal = 16.dp),
text = stringResource(id = R.string.search_hint),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
androidx.compose.animation.AnimatedVisibility(focused, enter = fadeIn(), exit = fadeOut()) {
Box(Modifier.fillMaxSize().padding(8.dp)) {
IconButton(
modifier = Modifier.align(Alignment.TopStart),
onClick = {
focused = false
}
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "close search bar"
)
}
}
}
}
}
}
}
@Composable
fun MainScreen(
contentType: ContentType,
displayFeatures: List<DisplayFeature>,
uiState: MainUIState,
closeDetailScreen: () -> Unit
) {
LaunchedEffect(contentType) {
if (contentType == ContentType.SINGLE_PANE && !uiState.isDetailOnlyOpen) {
closeDetailScreen()
}
}
val galleryLazyListState = rememberLazyListState()
if (contentType == ContentType.DUAL_PANE) {
TwoPane(
first = {
GalleryList(
contentType = contentType,
galleryLazyListState = galleryLazyListState
)
},
second = {
},
strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f, gapWidth = 16.dp),
displayFeatures = displayFeatures
)
} else {
GalleryList(
contentType = contentType,
galleryLazyListState = galleryLazyListState
)
}
}
@Composable
fun GalleryList(
contentType: ContentType,
galleries: List<GalleryInfo> = emptyList(),
openedGallery: GalleryInfo? = null,
query: SearchQuery? = null,
onQueryChange: (SearchQuery) -> Unit = {},
onSearch: () -> Unit = { },
selectedGalleryIds: Set<Int> = emptySet(),
toggleGallerySelection: (Int) -> Unit = {},
galleryLazyListState: LazyListState,
navigateToDetails: (GalleryInfo, ContentType) -> Unit = { gi, ct -> }
) {
var topOffset by remember { mutableIntStateOf(0) }
SearchBar(
contentType = contentType,
query = query,
onQueryChange = onQueryChange,
onSearch = onSearch,
topOffset = topOffset,
onTopOffsetChange = { topOffset = it },
) {
}
}

View File

@@ -3,6 +3,7 @@ package xyz.quaver.pupil.ui.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import xyz.quaver.pupil.networking.GalleryInfo
import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.composable.MainDestination
import xyz.quaver.pupil.ui.composable.mainDestinations
@@ -16,10 +17,22 @@ class MainViewModel : ViewModel() {
currentDestination = destination
)
}
fun closeDetailScreen() {
_uiState.value = _uiState.value.copy(
isDetailOnlyOpen = false
)
}
fun navigateToDetail() {
}
}
data class MainUIState(
val currentDestination: MainDestination = mainDestinations.first(),
val query: SearchQuery? = null,
val loading: Boolean = true
val loading: Boolean = true,
val openedGallery: GalleryInfo? = null,
val isDetailOnlyOpen: Boolean = false
)