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:
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,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"
|
||||
|
||||
Reference in new issue
Block a user