From 39b8bbc7250cfde9811dc1510d3a95779c0da9b1 Mon Sep 17 00:00:00 2001 From: tom5079 <7948651+tom5079@users.noreply.github.com> Date: Sat, 24 Feb 2024 10:24:36 -0800 Subject: [PATCH] Search bar skeleton --- .../quaver/pupil/networking/GalleryInfo.kt | 84 ++++++++ .../pupil/networking/HitomiHttpClient.kt | 3 +- .../quaver/pupil/networking/SearchQuery.kt | 4 +- .../java/xyz/quaver/pupil/ui/MainActivity.kt | 3 +- .../quaver/pupil/ui/composable/GalleryList.kt | 10 - .../xyz/quaver/pupil/ui/composable/MainApp.kt | 30 ++- .../quaver/pupil/ui/composable/MainScreen.kt | 204 ++++++++++++++++++ .../pupil/ui/viewmodel/MainViewModel.kt | 15 +- 8 files changed, 331 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/GalleryList.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/MainScreen.kt diff --git a/app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt b/app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt new file mode 100644 index 00000000..4739fc67 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt @@ -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? = null, + val groups: List? = null, + @SerialName("parodys") val series: List? = null, + val tags: List? = null, + val related: List = emptyList(), + val languages: List = emptyList(), + val characters: List? = null, + @SerialName("scene_indexes") val sceneIndices: List? = emptyList(), + val files: List = emptyList() +) diff --git a/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt index 6ec8cd21..14c2af48 100644 --- a/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt +++ b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt @@ -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 diff --git a/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt index f242d6e1..c1a77c1e 100644 --- a/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt +++ b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt @@ -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 } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt index 22ab0eca..54a298fb 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -50,7 +50,8 @@ class MainActivity : BaseActivity() { windowSize = windowSize, displayFeatures = displayFeatures, uiState = uiState, - navigateToDestination = viewModel::navigateToDestination + navigateToDestination = viewModel::navigateToDestination, + closeDetailScreen = viewModel::closeDetailScreen ) } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/GalleryList.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/GalleryList.kt deleted file mode 100644 index e4dac9c2..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/composable/GalleryList.kt +++ /dev/null @@ -1,10 +0,0 @@ -package xyz.quaver.pupil.ui.composable - -import androidx.compose.runtime.Composable - -@Composable -fun GalleryList( - -) { - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt index 8c24d21e..2d0b24e1 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt @@ -27,7 +27,8 @@ fun MainApp( windowSize: WindowSizeClass, displayFeatures: List, 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, 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, diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainScreen.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainScreen.kt new file mode 100644 index 00000000..a4d5eb09 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainScreen.kt @@ -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, + 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 = emptyList(), + openedGallery: GalleryInfo? = null, + query: SearchQuery? = null, + onQueryChange: (SearchQuery) -> Unit = {}, + onSearch: () -> Unit = { }, + selectedGalleryIds: Set = 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 }, + ) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt index 35e68460..381f5af8 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt @@ -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 ) \ No newline at end of file