Compose GalleryListScene (#301)

* Initial Migrate

* Cleanup

* Search

* Refine

* Initial searchLayout

* Cleanup

* Margins

* Animated State

* Animated State

* Fixes

* ShowSearchFab

* Refine

* Log

* Refine naming

* Update

* Scroll to hide FAB

* Cleanups

* Initial side sheet

* Refine side sheet

* Refine layout

* Refine transition

* Refine padding

* Refine ProvideSideSheetContent to handle multiple screen on composition

* Let sidesheet container padding vec insets

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
Co-authored-by: skymkmk <admin@skymkmk.com>
This commit is contained in:
authored and GitHub committed 2023-11-23 14:14:02 +00:00
1 parent 42d17621f9
commit c9c06202bb
24 files changed
+1322 -1750

No files matched your search

@@ -0,0 +1,213 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
@Composable
fun rememberDragDropState(
lazyListState: LazyListState,
onMove: (Int, Int) -> Unit,
): DragDropState {
val scope = rememberCoroutineScope()
val state = remember(lazyListState) {
DragDropState(
state = lazyListState,
onMove = onMove,
scope = scope,
)
}
LaunchedEffect(state) {
while (true) {
val diff = state.scrollChannel.receive()
lazyListState.scrollBy(diff)
}
}
return state
}
class DragDropState internal constructor(
private val state: LazyListState,
private val scope: CoroutineScope,
private val onMove: (Int, Int) -> Unit,
) {
var draggingItemIndex by mutableStateOf<Int?>(null)
private set
internal val scrollChannel = Channel<Float>()
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
private var draggingItemInitialOffset by mutableIntStateOf(0)
internal val draggingItemOffset: Float
get() = draggingItemLayoutInfo?.let { item ->
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
} ?: 0f
private val draggingItemLayoutInfo: LazyListItemInfo?
get() = state.layoutInfo.visibleItemsInfo
.firstOrNull { it.index == draggingItemIndex }
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
internal var previousItemOffset = Animatable(0f)
private set
internal fun onDragStart(offset: Offset) {
state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
offset.y.toInt() in item.offset..(item.offset + item.size)
}?.also {
draggingItemIndex = it.index
draggingItemInitialOffset = it.offset
}
}
internal fun onDragInterrupted() {
if (draggingItemIndex != null) {
previousIndexOfDraggedItem = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
previousItemOffset.snapTo(startOffset)
previousItemOffset.animateTo(
0f,
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 1f,
),
)
previousIndexOfDraggedItem = null
}
}
draggingItemDraggedDelta = 0f
draggingItemIndex = null
draggingItemInitialOffset = 0
}
internal fun onDrag(offset: Offset) {
draggingItemDraggedDelta += offset.y
val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset + draggingItemOffset
val endOffset = startOffset + draggingItem.size
val middleOffset = startOffset + (endOffset - startOffset) / 2f
val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
middleOffset.toInt() in item.offset..item.offsetEnd &&
draggingItem.index != item.index
}
if (targetItem != null) {
val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
draggingItem.index
} else if (draggingItem.index == state.firstVisibleItemIndex) {
targetItem.index
} else {
null
}
if (scrollToIndex != null) {
scope.launch {
// this is needed to neutralize automatic keeping the first item first.
state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
onMove.invoke(draggingItem.index, targetItem.index)
}
} else {
onMove.invoke(draggingItem.index, targetItem.index)
}
draggingItemIndex = targetItem.index
} else {
val overscroll = when {
draggingItemDraggedDelta > 0 ->
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
draggingItemDraggedDelta < 0 ->
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
else -> 0f
}
if (overscroll != 0f) {
scrollChannel.trySend(overscroll)
}
}
}
private val LazyListItemInfo.offsetEnd: Int
get() = this.offset + this.size
}
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
return this.pointerInput(dragDropState) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
dragDropState.onDrag(offset = offset)
},
onDragStart = { offset -> dragDropState.onDragStart(offset) },
onDragEnd = { dragDropState.onDragInterrupted() },
onDragCancel = { dragDropState.onDragInterrupted() },
)
}
}
@ExperimentalFoundationApi
@Composable
fun LazyItemScope.DraggableItem(
dragDropState: DragDropState,
index: Int,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit,
) {
val dragging = index == dragDropState.draggingItemIndex
val draggingModifier = if (dragging) {
Modifier
.zIndex(1f)
.graphicsLayer {
translationY = dragDropState.draggingItemOffset
}
} else if (index == dragDropState.previousIndexOfDraggedItem) {
Modifier.zIndex(1f)
.graphicsLayer {
translationY = dragDropState.previousItemOffset.value
}
} else {
Modifier.animateItemPlacement()
}
Column(modifier = modifier.then(draggingModifier)) {
content(dragging)
}
}
@@ -129,6 +129,7 @@ suspend fun <T> fetchCompat(
reqBody: RequestBody? = null,
parser: suspend (ByteBuffer) -> T,
): T {
Log.d(TAG, url)
fun commonChecks(body: ByteBuffer) {
// Check sad panda(without panda)
if (!body.hasRemaining()) {
@@ -48,23 +48,6 @@ data class ListUrlBuilder(
var isOnlySearchCovers: Boolean = false,
) : Parcelable {
fun reset() {
mode = MODE_NORMAL
mPrev = null
mNext = null
mJumpTo = null
this.category = EhUtils.NONE
mKeyword = null
advanceSearch = -1
minRating = -1
pageFrom = -1
pageTo = -1
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
hash = null
}
fun setIndex(index: String?, isNext: Boolean = true) {
mNext = index?.takeIf { isNext }
mPrev = index?.takeUnless { isNext }
@@ -80,8 +63,7 @@ data class ListUrlBuilder(
mKeyword = keyword
}
fun set(q: QuickSearch) {
reset()
constructor(q: QuickSearch) : this() {
mode = q.mode
this.category = q.category
mKeyword = q.keyword
@@ -134,9 +116,8 @@ data class ListUrlBuilder(
/**
* @param query xxx=yyy&mmm=nnn
*/
fun setQuery(query: String?) {
constructor(query: String?) : this() {
// TODO page
reset()
if (query.isNullOrEmpty()) {
return
}
@@ -42,9 +42,7 @@ object GalleryListUrlParser {
}
val path = url.path ?: return null
return if (PATH_NORMAL == path || path.isEmpty()) {
val builder = ListUrlBuilder()
builder.setQuery(url.query)
builder
ListUrlBuilder(url.query)
} else if (path.startsWith(PATH_UPLOADER)) {
parseUploader(path)
} else if (path.startsWith(PATH_TAG)) {
@@ -57,10 +55,7 @@ object GalleryListUrlParser {
} catch (e: NumberFormatException) {
return null
}
val builder = ListUrlBuilder()
builder.setQuery(url.query)
builder.category = category
builder
ListUrlBuilder(url.query).also { it.category = category }
} else {
null
}
@@ -43,6 +43,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.verticalScroll
@@ -69,6 +70,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
@@ -105,7 +107,7 @@ import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.GalleryListFragment.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.ProgressFragment
import com.hippo.ehviewer.ui.scene.TokenArgs
import com.hippo.ehviewer.ui.scene.navAnimated
@@ -151,21 +153,13 @@ class MainActivity : EhActivity() {
private lateinit var navController: NavController
private val isInitializedFlow = MutableStateFlow(false)
private var sideSheet by mutableStateOf<(@Composable ColumnScope.(DrawerState2) -> Unit)?>(null)
private var sideSheet = mutableStateListOf<@Composable ColumnScope.(DrawerState2) -> Unit>()
@Composable
fun ProvideSideSheetContent(content: @Composable ColumnScope.(DrawerState2) -> Unit) {
DisposableEffect(content) {
require(sideSheet == null) {
"Provide SideSheet content when previous content not released!!!"
}
sideSheet = content
onDispose {
require(sideSheet == content) {
"Try to release SideSheetContent not belong to us"
}
sideSheet = null
}
sideSheet.add(0, content)
onDispose { sideSheet.remove(content) }
}
}
@@ -354,7 +348,7 @@ class MainActivity : EhActivity() {
drawerState = drawerState,
gesturesEnabled = !drawerLocked || drawerState.isOpen,
) {
val sheet = sideSheet
val sheet = sideSheet.firstOrNull()
val sideDrawerState = rememberDrawerState(DrawerValue.Closed)
LaunchedEffect(Unit) {
openSideSheetFlow.collectLatest {
@@ -369,10 +363,10 @@ class MainActivity : EhActivity() {
SideDrawer(
drawerContent = {
if (sheet != null) {
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.End)
ModalDrawerSheet(
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 112).dp),
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 112).dp).windowInsetsPadding(insets),
drawerShape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.End),
) {
sheet(sideDrawerState)
}
@@ -1,46 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.widget.FrameLayout
class BringOutTransition(contentLayout: FrameLayout, mSearchLayout: SearchLayout) :
ViewTransition(contentLayout, mSearchLayout) {
override fun startAnimations(hiddenView: View, shownView: View) {
mAnimator1 = hiddenView.animate().alpha(0f).scaleY(0.7f).scaleX(0.7f).apply {
setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
hiddenView.visibility = View.GONE
mAnimator1 = null
}
}).start()
}
shownView.alpha = 0f
shownView.scaleX = 0.7f
shownView.scaleY = 0.7f
shownView.visibility = View.VISIBLE
mAnimator2 = shownView.animate().alpha(1f).scaleX(1f).scaleY(1f).apply {
setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mAnimator2 = null
}
}).start()
}
}
}
@@ -1,29 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.StaggeredGridLayoutManager
object LayoutManagerUtils {
val LayoutManager.firstVisibleItemPosition: Int
get() = when (this) {
is LinearLayoutManager -> findFirstVisibleItemPosition()
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null).min()
else -> error("Can't do getFirstVisibleItemPosition for " + javaClass.name)
}
}
@@ -1,74 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
class MarginItemDecoration(
margin: Int,
paddingLeft: Int,
paddingTop: Int,
paddingRight: Int,
paddingBottom: Int,
) : ItemDecoration() {
private var mMargin = 0
private var mPaddingLeft = 0
private var mPaddingTop = 0
private var mPaddingRight = 0
private var mPaddingBottom = 0
init {
setMargin(margin, paddingLeft, paddingTop, paddingRight, paddingBottom)
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect[mMargin, mMargin, mMargin] = mMargin
}
/**
* @param margin gap between two item
* @param paddingLeft gap between RecyclerView left and left item left
* @param paddingTop gap between RecyclerView top and top item top
* @param paddingRight gap between RecyclerView right and right item right
* @param paddingBottom gap between RecyclerView bottom and bottom item bottom
*/
fun setMargin(
margin: Int,
paddingLeft: Int,
paddingTop: Int,
paddingRight: Int,
paddingBottom: Int,
) {
val halfMargin = margin / 2
mMargin = halfMargin
mPaddingLeft = paddingLeft - halfMargin
mPaddingTop = paddingTop - halfMargin
mPaddingRight = paddingRight - halfMargin
mPaddingBottom = paddingBottom - halfMargin
}
fun applyPaddings(view: View) {
view.setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom)
}
}
@@ -1,207 +0,0 @@
package com.hippo.ehviewer.ui.legacy
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.AttributeSet
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.get
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.image.Image.Companion.decodeBitmap
import com.hippo.ehviewer.ui.main.AdvancedSearchOption
import com.hippo.ehviewer.ui.main.ImageSearch
import com.hippo.ehviewer.ui.main.NormalSearch
import com.hippo.ehviewer.ui.main.SearchAdvanced
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.pickVisualMedia
import kotlinx.coroutines.launch
class SearchViewModel : ViewModel() {
var isNormalMode by mutableStateOf(true) // else ImageSearch mode
var isAdvancedMode by mutableStateOf(false)
var mCategory by mutableIntStateOf(Settings.searchCategory)
var mSearchMode by mutableIntStateOf(1)
var advancedState by mutableStateOf(AdvancedSearchOption())
var uss by mutableStateOf(false)
var osc by mutableStateOf(false)
var path by mutableStateOf("")
}
class SearchLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
private val vm by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SearchViewModel>()
}
@Composable
override fun Content() {
val coroutineScope = rememberCoroutineScope()
val windowSizeClass = calculateWindowSizeClass(activity = context.findActivity())
fun selectImage() = coroutineScope.launch {
context.pickVisualMedia(ActivityResultContracts.PickVisualMedia.ImageOnly)?.let {
vm.path = it.toString()
}
}
Mdc3Theme {
Column(
modifier = Modifier.imePadding().statusBarsPadding().padding(top = 72.dp)
.verticalScroll(rememberScrollState()).navigationBarsPadding().padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)),
) {
AnimatedVisibility(visible = vm.isNormalMode) {
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
Text(text = stringResource(id = R.string.search_normal), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
NormalSearch(
category = vm.mCategory,
onCategoryChanged = {
Settings.searchCategory = it
vm.mCategory = it
},
searchMode = vm.mSearchMode,
onSearchModeChanged = { vm.mSearchMode = it },
isAdvanced = vm.isAdvancedMode,
onAdvancedChanged = { vm.isAdvancedMode = it },
showInfo = { BaseDialogBuilder(context).setMessage(R.string.search_tip).show() },
maxItemsInEachRow = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 2
WindowWidthSizeClass.Medium -> 3
else -> 5
},
)
}
}
}
AnimatedVisibility(visible = vm.isNormalMode && vm.isAdvancedMode) {
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
Text(text = stringResource(id = R.string.search_advance), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
SearchAdvanced(
state = vm.advancedState,
onStateChanged = { vm.advancedState = it },
)
}
}
}
AnimatedVisibility(visible = !vm.isNormalMode) {
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
Text(text = stringResource(id = R.string.search_image), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
ImageSearch(
imagePath = vm.path,
onSelectImage = ::selectImage,
uss = vm.uss,
onUssChecked = { vm.uss = it },
osc = vm.osc,
onOscChecked = { vm.osc = it },
)
}
}
}
SecondaryTabRow(
selectedTabIndex = if (vm.isNormalMode) 0 else 1,
divider = {},
) {
Tab(
selected = vm.isNormalMode,
onClick = { vm.isNormalMode = true },
text = { Text(text = stringResource(id = R.string.keyword_search)) },
)
Tab(
selected = !vm.isNormalMode,
onClick = { vm.isNormalMode = false },
text = { Text(text = stringResource(id = R.string.search_image)) },
)
}
}
}
}
fun setSearchMyTags(isMyTags: Boolean) {
if (isMyTags) vm.mSearchMode = 2
}
fun setCategory(category: Int) {
vm.mCategory = category
}
fun formatListUrlBuilder(urlBuilder: ListUrlBuilder, query: String?) {
urlBuilder.reset()
when (vm.isNormalMode) {
true -> {
when (vm.mSearchMode) {
1 -> urlBuilder.mode = ListUrlBuilder.MODE_NORMAL
2 -> urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION
3 -> urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER
4 -> urlBuilder.mode = ListUrlBuilder.MODE_TAG
}
urlBuilder.keyword = query
urlBuilder.category = vm.mCategory
if (vm.isAdvancedMode) {
urlBuilder.advanceSearch = vm.advancedState.advanceSearch
urlBuilder.minRating = vm.advancedState.minRating
val pageFrom = vm.advancedState.fromPage
val pageTo = vm.advancedState.toPage
if (pageTo != -1 && pageTo < 10) {
throw EhException(context.getString(R.string.search_sp_err1))
} else if (pageFrom != -1 && pageTo != -1 && pageTo - pageFrom < 20) {
throw EhException(context.getString(R.string.search_sp_err2))
}
urlBuilder.pageFrom = pageFrom
urlBuilder.pageTo = pageTo
}
}
false -> {
urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
if (vm.path.isBlank()) throw EhException(context.getString(R.string.select_image_first))
val uri = Uri.parse(vm.path)
val temp = AppConfig.createTempFile() ?: return
val bitmap = context.decodeBitmap(uri) ?: return
temp.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
urlBuilder.imagePath = temp.path
urlBuilder.isUseSimilarityScan = vm.uss
urlBuilder.isOnlySearchCovers = vm.osc
}
}
}
}
@@ -1,5 +1,8 @@
package com.hippo.ehviewer.ui.main
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
object AdvanceTable {
const val SH = 0x1
const val STO = 0x2
@@ -8,9 +11,10 @@ object AdvanceTable {
const val SFT = 0x400
}
@Parcelize
data class AdvancedSearchOption(
val advanceSearch: Int = 0,
val minRating: Int = -1,
val fromPage: Int = -1,
val toPage: Int = -1,
)
) : Parcelable
@@ -65,7 +65,7 @@ fun FabLayout(
val coroutineScope = rememberCoroutineScope()
val appearState by animateFloatAsState(
targetValue = if (hidden) 0f else 1f,
animationSpec = tween(FAB_ANIMATE_TIME),
animationSpec = tween(FAB_ANIMATE_TIME, if (hidden) 0 else FAB_ANIMATE_TIME),
label = "hiddenState",
)
Box(
@@ -1,140 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntDef
import androidx.compose.ui.platform.ComposeView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import arrow.core.partially1
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.detailSize
import com.hippo.ehviewer.Settings.thumbSizeDp
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
import com.hippo.ehviewer.ui.legacy.MarginItemDecoration
import com.hippo.ehviewer.ui.legacy.STRATEGY_MIN_SIZE
import com.hippo.ehviewer.ui.legacy.STRATEGY_SUITABLE_SIZE
import com.hippo.ehviewer.util.dp2px
import splitties.init.appCtx
private val diffCallback = object : DiffUtil.ItemCallback<BaseGalleryInfo>() {
override fun areItemsTheSame(oldItem: BaseGalleryInfo, newItem: BaseGalleryInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: BaseGalleryInfo, newItem: BaseGalleryInfo) = oldItem == newItem
}
class GalleryAdapter(
private val recyclerView: RecyclerView,
private val showFavorite: Boolean,
private val onItemClick: (BaseGalleryInfo) -> Unit,
private val onItemLongClick: (BaseGalleryInfo) -> Unit,
) : PagingDataAdapter<BaseGalleryInfo, GalleryHolder>(diffCallback) {
private val resources = recyclerView.context.resources
private val layoutManager: AutoStaggeredGridLayoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
private var mListDecoration: ItemDecoration? = null
private var mGirdDecoration: MarginItemDecoration? = null
private var mType = TYPE_INVALID
var tracker: GallerySelectionTracker<BaseGalleryInfo>? = null
var type: Int
get() = mType
set(type) {
if (type == mType) return
mType = type
when (type) {
TYPE_LIST -> {
val columnWidth = resources.getDimensionPixelOffset(
when (detailSize) {
0 -> R.dimen.gallery_list_column_width_long
1 -> R.dimen.gallery_list_column_width_short
else -> error("Unexpected value: $detailSize")
},
)
layoutManager.setColumnSize(columnWidth)
layoutManager.setStrategy(STRATEGY_MIN_SIZE)
if (null != mGirdDecoration) {
recyclerView.removeItemDecoration(mGirdDecoration!!)
mGirdDecoration = null
}
if (null == mListDecoration) {
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
mListDecoration = object : ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.set(0, interval / 2, 0, interval / 2)
}
}
recyclerView.addItemDecoration(mListDecoration!!)
}
}
TYPE_GRID -> {
val columnWidth = dp2px(appCtx, thumbSizeDp.toFloat())
layoutManager.setColumnSize(columnWidth)
layoutManager.setStrategy(STRATEGY_SUITABLE_SIZE)
if (null != mListDecoration) {
recyclerView.removeItemDecoration(mListDecoration!!)
mListDecoration = null
}
if (null == mGirdDecoration) {
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_grid_interval)
mGirdDecoration = MarginItemDecoration(interval, 0, 0, 0, 0)
recyclerView.addItemDecoration(mGirdDecoration!!)
}
}
}
refresh()
}
init {
recyclerView.adapter = withLoadStateHeaderAndFooter(
GalleryLoadStateAdapter(::retry),
GalleryLoadStateAdapter(::retry),
)
recyclerView.layoutManager = layoutManager
type = Settings.listMode
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
TYPE_LIST -> ListGalleryHolder(ComposeView(parent.context), showFavorite)
TYPE_GRID -> GridGalleryHolder(ComposeView(parent.context))
else -> error("Unexpected value: $viewType")
}
override fun getItemViewType(position: Int) = mType
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
val gi = getItem(position) ?: return
val selected = tracker?.isSelected(gi.gid) ?: false
holder.galleryId = gi.gid
holder.bind(gi, selected, onItemClick.partially1(gi), onItemLongClick.partially1(gi))
}
@IntDef(TYPE_LIST, TYPE_GRID)
@Retention(AnnotationRetention.SOURCE)
annotation class Type
companion object {
const val TYPE_INVALID = -1
const val TYPE_LIST = 0
const val TYPE_GRID = 1
}
}
@@ -110,7 +110,7 @@ import com.hippo.ehviewer.ui.jumpToReaderByPage
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
import com.hippo.ehviewer.ui.main.GalleryCommentCard
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.GalleryListFragment.Companion.toStartArgs
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
import com.hippo.ehviewer.ui.tools.normalizeSpan
@@ -153,7 +153,7 @@ import com.hippo.ehviewer.ui.main.GalleryTags
import com.hippo.ehviewer.ui.modifyFavorites
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.GalleryListFragment.Companion.toStartArgs
import com.hippo.ehviewer.ui.startDownload
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconButton
@@ -1,92 +1,12 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.recyclerview.widget.RecyclerView
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.listThumbSize
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.setMD3Content
import eu.kanade.tachiyomi.util.system.pxToDp
abstract class GalleryHolder(composeView: ComposeView) : RecyclerView.ViewHolder(composeView) {
var galleryId = 0L
abstract fun bind(
galleryInfo: GalleryInfo,
isChecked: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
)
}
class ListGalleryHolder(private val composeView: ComposeView, private val showFavourite: Boolean) : GalleryHolder(composeView) {
private val height = (3 * listThumbSize * 3).pxToDp.dp
private val showPages = Settings.showGalleryPages
override fun bind(galleryInfo: GalleryInfo, isChecked: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
composeView.setMD3Content {
CheckableItem(checked = isChecked) {
// Workaround ComposeView in RecyclerView, since view is reused, remembered value will not update when setContent with similar @Composable lambda, since composer group key is the same
key(galleryInfo.gid) {
GalleryInfoListItem(
onClick = onClick,
onLongClick = onLongClick,
info = galleryInfo,
modifier = Modifier.height(height),
isInFavScene = !showFavourite,
showPages = showPages,
)
}
}
}
}
}
class GridGalleryHolder(private val composeView: ComposeView) : GalleryHolder(composeView) {
override fun bind(galleryInfo: GalleryInfo, isChecked: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
composeView.setMD3Content {
CheckableItem(checked = isChecked) {
GalleryInfoGridItem(
onClick = onClick,
onLongClick = onLongClick,
info = galleryInfo,
)
}
}
// Workaround https://github.com/Ehviewer-Overhauled/Ehviewer/issues/1023
// See https://issuetracker.google.com/issues/240449681
composeView.getChildAt(0)?.requestLayout()
}
}
@Composable
fun CheckableItem(checked: Boolean, content: @Composable () -> Unit) {
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -2,8 +2,6 @@ package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.MotionEvent
import androidx.core.util.component1
import androidx.core.util.component2
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.SelectionTracker
@@ -24,15 +22,7 @@ class GallerySelectionTracker<T : GalleryInfo>(
selectionId,
recyclerView,
object : ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int) =
if (adapter != null) {
val (wrappedAdapter, localPosition) = adapter.getWrappedAdapterAndPosition(position)
(wrappedAdapter as? GalleryAdapter)?.let {
getItems()[localPosition].gid
}
} else {
getItems()[position].gid
}
override fun getKey(position: Int) = getItems()[position].gid
override fun getPosition(key: Long) =
if (adapter != null) {
@@ -54,7 +44,6 @@ class GallerySelectionTracker<T : GalleryInfo>(
},
StorageStrategy.createLongStorage(),
).apply(builder).build()
val selectionSize get() = tracker.selection.size()
var isInCustomChoice = false
private set
@@ -32,8 +32,8 @@ fun NavController.navWithUrl(url: String): Boolean {
navAnimated(
R.id.galleryListScene,
bundleOf(
GalleryListScene.KEY_ACTION to GalleryListScene.ACTION_LIST_URL_BUILDER,
GalleryListScene.KEY_LIST_URL_BUILDER to it,
GalleryListFragment.KEY_ACTION to GalleryListFragment.ACTION_LIST_URL_BUILDER,
GalleryListFragment.KEY_LIST_URL_BUILDER to it,
),
)
return true
@@ -130,13 +130,14 @@ fun SearchBarScreen(
searchFieldState: TextFieldState,
searchFieldHint: String? = null,
showSearchFab: Boolean = false,
onApplySearch: (String) -> Unit,
onApplySearch: suspend (String) -> Unit,
onSearchExpanded: () -> Unit,
onSearchHidden: () -> Unit,
refreshState: PullToRefreshState? = null,
suggestionProvider: SuggestionProvider? = null,
searchBarOffsetY: Int,
trailingIcon: @Composable () -> Unit,
snackbarHost: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
var mSuggestionList by remember { mutableStateOf(emptyList<Suggestion>()) }
@@ -232,7 +233,9 @@ fun SearchBarScreen(
mSearchDatabase.insert(search)
}
}
onApplySearch(query)
scope.launchIO {
onApplySearch(query)
}
}
fun deleteKeyword(keyword: String) {
@@ -267,10 +270,11 @@ fun SearchBarScreen(
.height(SearchBarDefaults.InputFieldHeight + 16.dp),
)
},
snackbarHost = snackbarHost,
floatingActionButton = {
val hiddenState by animateFloatAsState(
targetValue = if (showSearchFab) 1f else 0f,
animationSpec = tween(FAB_ANIMATE_TIME),
animationSpec = tween(FAB_ANIMATE_TIME, if (showSearchFab) FAB_ANIMATE_TIME else 0),
label = "hiddenState",
)
FloatingActionButton(
@@ -2,6 +2,7 @@ package com.hippo.ehviewer.ui.tools
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
@@ -162,6 +163,75 @@ class DialogState {
}
}
suspend fun awaitInputTextWithCheckBox(
initial: String = "",
@StringRes title: Int? = null,
@StringRes hint: Int? = null,
checked: Boolean,
@StringRes checkBoxText: Int,
isNumber: Boolean = false,
invalidator: (suspend (String, Boolean) -> String?)? = null,
): Pair<String, Boolean> {
return dialog { cont ->
val coroutineScope = rememberCoroutineScope()
var state by remember(cont) { mutableStateOf(initial) }
var error by remember(cont) { mutableStateOf<String?>(null) }
var checkedState by remember { mutableStateOf(checked) }
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = {
if (invalidator == null) {
cont.resume(state to checkedState)
} else {
coroutineScope.launch {
error = invalidator.invoke(state, checkedState)
error ?: cont.resume(state to checkedState)
}
}
}) {
Text(text = stringResource(id = android.R.string.ok))
}
},
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedTextField(
value = state,
onValueChange = { state = it },
label = hint.ifNotNullThen {
Text(text = stringResource(id = hint!!))
},
trailingIcon = error.ifNotNullThen {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = null,
)
},
supportingText = error.ifNotNullThen {
Text(text = error!!)
},
isError = error != null,
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
)
Row(
modifier = Modifier.clickable { checkedState = !checkedState }.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = checkedState,
onCheckedChange = { checkedState = !checkedState },
)
Text(text = stringResource(checkBoxText))
}
}
},
)
}
}
suspend fun awaitPermissionOrCancel(
@StringRes confirmText: Int = android.R.string.ok,
@StringRes dismissText: Int = android.R.string.cancel,
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<include
android:id="@+id/content_layout"
layout="@layout/widget_content_layout" />
<com.hippo.ehviewer.ui.legacy.SearchLayout
android:id="@+id/search_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/gallery_search_bar_margin_v"
android:layout_marginEnd="@dimen/gallery_search_bar_margin_v" />
<com.hippo.ehviewer.ui.legacy.FabLayout
android:id="@+id/fab_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</merge>
@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2015 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/content_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
style="@style/ProgressView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<TextView
android:id="@+id/tip"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="228dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="8dp"
android:gravity="center_horizontal" />
<androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hippo.ehviewer.ui.legacy.IgnoreCancelRV
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/gallery_list_margin_h"
android:paddingVertical="@dimen/gallery_list_margin_v" />
</androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout>
<com.hippo.ehviewer.ui.legacy.FastScroller
android:id="@+id/fast_scroller"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:paddingStart="20dp"
android:paddingEnd="4dp"
android:paddingVertical="8dp" />
</FrameLayout>
+5 -5
View File
@@ -5,11 +5,11 @@
<fragment
android:id="@+id/galleryListScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryListScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryListFragment"
android:label="GalleryListScene" />
<fragment
android:id="@+id/nav_homepage"
android:name="com.hippo.ehviewer.ui.scene.GalleryListScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryListFragment"
android:label="GalleryListScene">
<argument
android:name="action"
@@ -18,7 +18,7 @@
</fragment>
<fragment
android:id="@+id/nav_subscription"
android:name="com.hippo.ehviewer.ui.scene.GalleryListScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryListFragment"
android:label="GalleryListScene">
<argument
android:name="action"
@@ -27,7 +27,7 @@
</fragment>
<fragment
android:id="@+id/nav_whats_hot"
android:name="com.hippo.ehviewer.ui.scene.GalleryListScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryListFragment"
android:label="GalleryListScene">
<argument
android:name="action"
@@ -36,7 +36,7 @@
</fragment>
<fragment
android:id="@+id/nav_toplist"
android:name="com.hippo.ehviewer.ui.scene.GalleryListScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryListFragment"
android:label="GalleryListScene">
<argument
android:name="action"