Search bar skeleton
This commit is contained in:
84
app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt
Normal file
84
app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt
Normal 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()
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ class MainActivity : BaseActivity() {
|
||||
windowSize = windowSize,
|
||||
displayFeatures = displayFeatures,
|
||||
uiState = uiState,
|
||||
navigateToDestination = viewModel::navigateToDestination
|
||||
navigateToDestination = viewModel::navigateToDestination,
|
||||
closeDetailScreen = viewModel::closeDetailScreen
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package xyz.quaver.pupil.ui.composable
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun GalleryList(
|
||||
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
204
app/src/main/java/xyz/quaver/pupil/ui/composable/MainScreen.kt
Normal file
204
app/src/main/java/xyz/quaver/pupil/ui/composable/MainScreen.kt
Normal 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 },
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user