Compose FabLayout (#290)
* ComposeFabLayout with Predictive Back * Refine * Fix padding * Fix anim * Fix scrim * Cleanup * Convert FavoritesScene to use ComposeFabLayout * Convert DownloadsScene to use ComposeFabLayout * Cleanup unused code * Rename ComposeFabLayout to FabLayout * Refine * Refine * Intercept MotionEvent.ACTION_CANCEL for rv have SelectionTracker attached * Revert to old icons * Revert FAB orders * Cleanup resources --------- Co-authored-by: Lokesh Gidra <lokeshgidra@google.com> Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
This commit is contained in:
20 files changed
+431
-1135
No files matched your search
@@ -0,0 +1,33 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
val EhIcons.Filled.GoTo by unsafeLazy {
|
||||
materialIcon(name = "GoTo") {
|
||||
materialPath {
|
||||
moveTo(13.0F, 13.0F)
|
||||
lineTo(17.0F, 17.0F)
|
||||
lineTo(21.0F, 13.0F)
|
||||
horizontalLineTo(18.0F)
|
||||
verticalLineTo(10.0F)
|
||||
curveTo(18.0F, 6.1F, 14.9F, 3.0F, 11.0F, 3.0F)
|
||||
reflectiveCurveTo(4.0F, 6.1F, 4.0F, 10.0F)
|
||||
verticalLineTo(17.0F)
|
||||
horizontalLineTo(6.0F)
|
||||
verticalLineTo(10.0F)
|
||||
curveTo(6.0F, 7.2F, 8.2F, 5.0F, 11.0F, 5.0F)
|
||||
reflectiveCurveTo(16.0F, 7.2F, 16.0F, 10.0F)
|
||||
verticalLineTo(13.0F)
|
||||
horizontalLineTo(13.0F)
|
||||
moveTo(16.0F, 19.0F)
|
||||
horizontalLineTo(18.0F)
|
||||
verticalLineTo(21.0F)
|
||||
horizontalLineTo(16.0F)
|
||||
verticalLineTo(19.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,482 +1,138 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.animation.Interpolator
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.util.AnimationUtils
|
||||
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
typealias OnExpandStateListener = (Boolean) -> Unit
|
||||
typealias SecondaryFab = Pair<ImageVector, suspend () -> Unit>
|
||||
|
||||
class FabLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : ViewGroup(context, attrs, defStyleAttr), View.OnClickListener {
|
||||
private var mFabSize = 0
|
||||
private var mFabMiniSize = 0
|
||||
private var mIntervalPrimary = 0
|
||||
private var mIntervalSecondary = 0
|
||||
private var mExpanded = true
|
||||
private var mAutoCancel = true
|
||||
private var mHidePrimaryFab = false
|
||||
private var mMainFabCenterY = -1f
|
||||
private var mOnExpandListeners = arrayListOf<OnExpandListener>()
|
||||
private var mOnClickFabListener: OnClickFabListener? = null
|
||||
private var fabAnimator: ViewPropertyAnimator? = null
|
||||
private var isHiding = false
|
||||
defStyle: Int = 0,
|
||||
) : AbstractComposeView(context, attrs, defStyle) {
|
||||
var secondaryFab by mutableStateOf<List<SecondaryFab>?>(null)
|
||||
var autoCancel by mutableStateOf(true)
|
||||
|
||||
private val mOnBackPressedCallback = object : OnBackPressedCallback(false), OnExpandListener {
|
||||
override fun handleOnBackPressed() {
|
||||
isExpanded = false
|
||||
}
|
||||
private var hidden by mutableStateOf(false)
|
||||
private val expandedBackingField = mutableStateOf(false)
|
||||
var expanded by expandedBackingField
|
||||
private val listeners = mutableListOf<OnExpandStateListener>()
|
||||
|
||||
override fun onExpand(expanded: Boolean) {
|
||||
isEnabled = expanded
|
||||
}
|
||||
}
|
||||
private val mFabAnimatorListener = object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
primaryFab.isVisible = false
|
||||
isHiding = false
|
||||
}
|
||||
fun addOnExpandStateListener(listener: OnExpandStateListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
(context as ComponentActivity).onBackPressedDispatcher.addCallback(mOnBackPressedCallback)
|
||||
mOnExpandListeners.add(mOnBackPressedCallback)
|
||||
fun show() {
|
||||
hidden = false
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
mOnBackPressedCallback.remove()
|
||||
mOnExpandListeners.remove(mOnBackPressedCallback)
|
||||
fun hide() {
|
||||
expanded = false
|
||||
hidden = true
|
||||
}
|
||||
|
||||
init {
|
||||
isSoundEffectsEnabled = false
|
||||
clipToPadding = false
|
||||
mFabSize = context.resources.getDimensionPixelOffset(R.dimen.fab_size)
|
||||
mFabMiniSize = context.resources.getDimensionPixelOffset(R.dimen.fab_min_size)
|
||||
mIntervalPrimary =
|
||||
context.resources.getDimensionPixelOffset(R.dimen.fab_layout_primary_margin)
|
||||
mIntervalSecondary =
|
||||
context.resources.getDimensionPixelOffset(R.dimen.fab_layout_secondary_margin)
|
||||
}
|
||||
|
||||
override fun addView(child: View, index: Int, params: LayoutParams) {
|
||||
check(child is FloatingActionButton) { "FloatingActionBarLayout should only " + "contain FloatingActionButton, but try to add " + child.javaClass.name }
|
||||
super.addView(child, index, params)
|
||||
}
|
||||
|
||||
val primaryFab: FloatingActionButton
|
||||
get() = getChildAt(childCount - 1) as FloatingActionButton
|
||||
|
||||
private val secondaryFabCount: Int
|
||||
get() = 0.coerceAtLeast(childCount - 1)
|
||||
|
||||
private fun getSecondaryFabAt(index: Int): FloatingActionButton? {
|
||||
return if (index < 0 || index >= secondaryFabCount) {
|
||||
null
|
||||
} else {
|
||||
getChildAt(index) as FloatingActionButton
|
||||
}
|
||||
}
|
||||
|
||||
fun setSecondaryFabVisibilityAt(index: Int, visible: Boolean) {
|
||||
getSecondaryFabAt(index)?.run {
|
||||
if (visible && visibility == GONE) {
|
||||
animate().cancel()
|
||||
visibility = if (mExpanded) VISIBLE else INVISIBLE
|
||||
} else if (!visible && visibility != GONE) {
|
||||
animate().cancel()
|
||||
visibility = GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChildMeasureSpec(parentMeasureSpec: Int): Int {
|
||||
val parentMode = MeasureSpec.getMode(parentMeasureSpec)
|
||||
val parentSize = MeasureSpec.getSize(parentMeasureSpec)
|
||||
val childMode: Int
|
||||
val childSize: Int
|
||||
when (parentMode) {
|
||||
MeasureSpec.AT_MOST, MeasureSpec.EXACTLY -> {
|
||||
childMode = MeasureSpec.AT_MOST
|
||||
childSize = parentSize
|
||||
}
|
||||
|
||||
MeasureSpec.UNSPECIFIED -> {
|
||||
childMode = MeasureSpec.UNSPECIFIED
|
||||
childSize = parentSize
|
||||
}
|
||||
|
||||
else -> {
|
||||
childMode = MeasureSpec.AT_MOST
|
||||
childSize = parentSize
|
||||
}
|
||||
}
|
||||
return MeasureSpec.makeMeasureSpec(childSize, childMode)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec)
|
||||
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec)
|
||||
measureChildren(childWidthMeasureSpec, childHeightMeasureSpec)
|
||||
var maxWidth = 0
|
||||
var maxHeight = 0
|
||||
val count = childCount
|
||||
for (i in 0 until count) {
|
||||
val child = getChildAt(i)
|
||||
if (child.visibility == GONE) {
|
||||
continue
|
||||
}
|
||||
maxWidth = maxWidth.coerceAtLeast(child.measuredWidth)
|
||||
maxHeight += child.measuredHeight
|
||||
}
|
||||
maxWidth += paddingLeft + paddingRight
|
||||
maxHeight += paddingTop + paddingBottom
|
||||
maxHeight = maxHeight.coerceAtLeast(suggestedMinimumHeight)
|
||||
maxWidth = maxWidth.coerceAtLeast(suggestedMinimumWidth)
|
||||
setMeasuredDimension(
|
||||
resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
|
||||
resolveSizeAndState(maxHeight, heightMeasureSpec, 0),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
var centerX = 0
|
||||
var bottom = measuredHeight - paddingBottom
|
||||
val count = childCount
|
||||
var i = count
|
||||
while (--i >= 0) {
|
||||
val child = getChildAt(i)
|
||||
if (child.visibility == GONE) {
|
||||
continue
|
||||
}
|
||||
val childWidth = child.measuredWidth
|
||||
val childHeight = child.measuredHeight
|
||||
var layoutBottom: Int
|
||||
var layoutRight: Int
|
||||
if (i == count - 1) {
|
||||
layoutBottom = bottom + (childHeight - mFabSize) / 2
|
||||
layoutRight = measuredWidth - paddingRight + (childWidth - mFabSize) / 2
|
||||
bottom -= mFabSize + mIntervalPrimary
|
||||
centerX = layoutRight - childWidth / 2
|
||||
mMainFabCenterY = layoutBottom - childHeight / 2f
|
||||
} else {
|
||||
layoutBottom = bottom + (childHeight - mFabMiniSize) / 2
|
||||
layoutRight = centerX + childWidth / 2
|
||||
bottom -= mFabMiniSize + mIntervalSecondary
|
||||
}
|
||||
child.layout(
|
||||
layoutRight - childWidth,
|
||||
layoutBottom - childHeight,
|
||||
layoutRight,
|
||||
layoutBottom,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addOnExpandListener(listener: OnExpandListener) {
|
||||
mOnExpandListeners.add(listener)
|
||||
}
|
||||
|
||||
fun setOnClickFabListener(listener: OnClickFabListener?) {
|
||||
mOnClickFabListener = listener
|
||||
if (listener != null) {
|
||||
var i = 0
|
||||
val n = childCount
|
||||
while (i < n) {
|
||||
getChildAt(i).setOnClickListener(this)
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
var i = 0
|
||||
val n = childCount
|
||||
while (i < n) {
|
||||
getChildAt(i).isClickable = false
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setHidePrimaryFab(hidePrimaryFab: Boolean) {
|
||||
if (mHidePrimaryFab != hidePrimaryFab) {
|
||||
mHidePrimaryFab = hidePrimaryFab
|
||||
val expanded = mExpanded
|
||||
val count = childCount
|
||||
if (!expanded && count > 0) {
|
||||
getChildAt(count - 1).visibility = if (hidePrimaryFab) INVISIBLE else VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoCancel(autoCancel: Boolean) {
|
||||
if (mAutoCancel != autoCancel) {
|
||||
mAutoCancel = autoCancel
|
||||
if (mExpanded) {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Mdc3Theme {
|
||||
if (expanded) {
|
||||
DisposableEffect(Unit) {
|
||||
listeners.forEach { it.invoke(true) }
|
||||
onDispose {
|
||||
listeners.forEach { it.invoke(false) }
|
||||
}
|
||||
}
|
||||
if (autoCancel) {
|
||||
setOnClickListener(this)
|
||||
} else {
|
||||
isClickable = false
|
||||
Spacer(
|
||||
modifier = Modifier.fillMaxSize().pointerInput(expanded) {
|
||||
detectTapGestures {
|
||||
expanded = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() {
|
||||
isExpanded = !mExpanded
|
||||
}
|
||||
|
||||
var isExpanded: Boolean
|
||||
get() = mExpanded
|
||||
set(expanded) {
|
||||
setExpanded(expanded, true)
|
||||
}
|
||||
|
||||
fun setExpanded(expanded: Boolean, animation: Boolean) {
|
||||
if (mExpanded != expanded) {
|
||||
mExpanded = expanded
|
||||
if (mAutoCancel) {
|
||||
if (expanded) {
|
||||
setOnClickListener(this)
|
||||
} else {
|
||||
isClickable = false
|
||||
}
|
||||
}
|
||||
val count = childCount
|
||||
if (count > 0) {
|
||||
if (mMainFabCenterY == -1f || !animation) {
|
||||
// It is before first onLayout
|
||||
val checkCount = if (mHidePrimaryFab) count else count - 1
|
||||
for (i in 0 until checkCount) {
|
||||
val child = getChildAt(i)
|
||||
if (child.visibility == GONE) {
|
||||
continue
|
||||
}
|
||||
child.visibility = if (expanded) VISIBLE else INVISIBLE
|
||||
if (expanded) {
|
||||
child.alpha = 1f
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val appearState by animateFloatAsState(
|
||||
targetValue = if (hidden) 0f else 1f,
|
||||
animationSpec = tween(FAB_ANIMATE_TIME),
|
||||
label = "hiddenState",
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val animatedProgress by animateFloatMergePredictiveBackAsState(expandedBackingField)
|
||||
FloatingActionButton(
|
||||
onClick = { expanded = !expanded },
|
||||
modifier = Modifier.rotate(lerp(-90f, 0f, appearState)).scale(appearState),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.rotate(lerp(135f, 0f, animatedProgress)),
|
||||
)
|
||||
}
|
||||
secondaryFab?.run {
|
||||
forEachIndexed { index, (imageVector, onClick) ->
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
coroutineScope.launchIO {
|
||||
runSuspendCatching {
|
||||
onClick()
|
||||
}
|
||||
expanded = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.width, placeable.height) {
|
||||
val distance = lerp(150 * (size - index) + 50, 0, animatedProgress)
|
||||
placeable.placeRelative(0, -distance, -(size - index).toFloat())
|
||||
}
|
||||
}.rotate(lerp(90f, 0f, appearState)).scale(appearState),
|
||||
) {
|
||||
Icon(imageVector = imageVector, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mHidePrimaryFab) {
|
||||
setPrimaryFabAnimation(getChildAt(count - 1), expanded, !expanded)
|
||||
}
|
||||
for (i in 0 until count - 1) {
|
||||
val child = getChildAt(i)
|
||||
if (child.visibility == GONE) {
|
||||
continue
|
||||
}
|
||||
setSecondaryFabAnimation(child, expanded, expanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
mOnExpandListeners.forEach { it.onExpand(expanded) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPrimaryFabAnimation(child: View, expanded: Boolean, delay: Boolean) {
|
||||
val startRotation: Float
|
||||
val endRotation: Float
|
||||
val startScale: Float
|
||||
val endScale: Float
|
||||
val interpolator: Interpolator
|
||||
if (expanded) {
|
||||
startRotation = -45.0f
|
||||
endRotation = 0.0f
|
||||
startScale = 0.0f
|
||||
endScale = 1.0f
|
||||
interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR
|
||||
} else {
|
||||
startRotation = 0.0f
|
||||
endRotation = 0.0f
|
||||
startScale = 1.0f
|
||||
endScale = 0.0f
|
||||
interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR
|
||||
}
|
||||
child.scaleX = startScale
|
||||
child.scaleY = startScale
|
||||
child.rotation = startRotation
|
||||
child.animate()
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale)
|
||||
.rotation(endRotation)
|
||||
.setStartDelay(if (delay) ANIMATE_TIME else 0L)
|
||||
.setDuration(ANIMATE_TIME)
|
||||
.setInterpolator(interpolator)
|
||||
.setListener(object : SimpleAnimatorListener() {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
if (expanded) {
|
||||
child.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (!expanded) {
|
||||
child.visibility = INVISIBLE
|
||||
}
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
private fun setSecondaryFabAnimation(child: View, expanded: Boolean, delay: Boolean) {
|
||||
val startTranslationY: Float
|
||||
val endTranslationY: Float
|
||||
val startAlpha: Float
|
||||
val endAlpha: Float
|
||||
val interpolator: Interpolator
|
||||
if (expanded) {
|
||||
startTranslationY = mMainFabCenterY - child.top - child.height / 2
|
||||
endTranslationY = 0f
|
||||
startAlpha = 0f
|
||||
endAlpha = 1f
|
||||
interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR
|
||||
} else {
|
||||
startTranslationY = 0f
|
||||
endTranslationY = mMainFabCenterY - child.top - child.height / 2
|
||||
startAlpha = 1f
|
||||
endAlpha = 0f
|
||||
interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR
|
||||
}
|
||||
child.alpha = startAlpha
|
||||
child.translationY = startTranslationY
|
||||
child.animate()
|
||||
.alpha(endAlpha)
|
||||
.translationY(endTranslationY)
|
||||
.setStartDelay(if (delay) ANIMATE_TIME else 0L)
|
||||
.setDuration(ANIMATE_TIME)
|
||||
.setInterpolator(interpolator)
|
||||
.setListener(object : SimpleAnimatorListener() {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
if (expanded) {
|
||||
child.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (!expanded) {
|
||||
child.visibility = INVISIBLE
|
||||
}
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (this === v) {
|
||||
isExpanded = false
|
||||
} else {
|
||||
mOnClickFabListener?.let {
|
||||
val position = indexOfChild(v)
|
||||
if (position == childCount - 1) {
|
||||
it.onClickPrimaryFab(this, v as FloatingActionButton)
|
||||
} else if (position >= 0 && mExpanded) {
|
||||
it.onClickSecondaryFab(this, v as FloatingActionButton, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchSetPressed(pressed: Boolean) {
|
||||
// Don't dispatch it to children
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val state = Bundle()
|
||||
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState())
|
||||
state.putBoolean(STATE_KEY_AUTO_CANCEL, mAutoCancel)
|
||||
state.putBoolean(STATE_KEY_EXPANDED, mExpanded)
|
||||
return state
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
if (state is Bundle) {
|
||||
super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER))
|
||||
setAutoCancel(state.getBoolean(STATE_KEY_AUTO_CANCEL))
|
||||
setExpanded(state.getBoolean(STATE_KEY_EXPANDED), false)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(animation: Boolean = true, delay: Long = 0L) {
|
||||
val fab = primaryFab
|
||||
if (!fab.isVisible) {
|
||||
fab.isVisible = true
|
||||
if (animation) {
|
||||
fabAnimator?.cancel()
|
||||
fab.rotation = ROTATION_DEGREE
|
||||
fabAnimator = fab.animate().scaleX(1f).scaleY(1f).rotation(0f)
|
||||
.setListener(null).setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).apply { start() }
|
||||
} else {
|
||||
fab.scaleX = 1f
|
||||
fab.scaleY = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hide(animation: Boolean = true): Long {
|
||||
val fab = primaryFab
|
||||
setExpanded(expanded = false, animation = true)
|
||||
return if (!fab.isVisible || isHiding) {
|
||||
0L
|
||||
} else {
|
||||
if (animation) {
|
||||
isHiding = true
|
||||
fabAnimator?.cancel()
|
||||
fabAnimator = fab.animate().scaleX(0f).scaleY(0f).rotation(-ROTATION_DEGREE)
|
||||
.setListener(mFabAnimatorListener).setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).apply { start() }
|
||||
ANIMATE_TIME
|
||||
} else {
|
||||
fab.isVisible = false
|
||||
fab.scaleX = 0f
|
||||
fab.scaleY = 0f
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun interface OnExpandListener {
|
||||
fun onExpand(expanded: Boolean)
|
||||
}
|
||||
|
||||
interface OnClickFabListener {
|
||||
fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton)
|
||||
fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ANIMATE_TIME = 300L
|
||||
private const val ROTATION_DEGREE = 90f
|
||||
private const val STATE_KEY_SUPER = "super"
|
||||
private const val STATE_KEY_AUTO_CANCEL = "auto_cancel"
|
||||
private const val STATE_KEY_EXPANDED = "expanded"
|
||||
}
|
||||
}
|
||||
|
||||
const val FAB_ANIMATE_TIME = 300
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class IgnoreCancelRV @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0,
|
||||
) : RecyclerView(context, attrs, defStyle) {
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(e: MotionEvent): Boolean {
|
||||
// Intercept MotionEvent.ACTION_CANCEL for rv have SelectionTracker attached
|
||||
// Otherwise predictive back will not work
|
||||
if (e.action == MotionEvent.ACTION_CANCEL) return false
|
||||
return super.onTouchEvent(e)
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@@ -73,7 +78,6 @@ import androidx.viewbinding.ViewBinding
|
||||
import arrow.core.partially1
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
@@ -98,8 +102,6 @@ import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.CheckBoxDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout.OnClickFabListener
|
||||
import com.hippo.ehviewer.ui.legacy.HandlerDrawable
|
||||
import com.hippo.ehviewer.ui.legacy.STRATEGY_MIN_SIZE
|
||||
import com.hippo.ehviewer.ui.legacy.ViewTransition
|
||||
@@ -119,15 +121,14 @@ import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.pxToDp
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import rikka.core.res.resolveColor
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
class DownloadsScene :
|
||||
SearchBarScene(),
|
||||
OnClickFabListener {
|
||||
class DownloadsScene : SearchBarScene() {
|
||||
/*---------------
|
||||
Whole life cycle
|
||||
---------------*/
|
||||
@@ -275,10 +276,12 @@ class DownloadsScene :
|
||||
{ (this as DownloadHolder).itemId },
|
||||
).apply {
|
||||
addCustomChoiceListener({
|
||||
binding.fabLayout.isExpanded = true
|
||||
binding.fabLayout.show()
|
||||
binding.fabLayout.expanded = true
|
||||
lockDrawer()
|
||||
}) {
|
||||
binding.fabLayout.isExpanded = false
|
||||
binding.fabLayout.expanded = false
|
||||
binding.fabLayout.hide()
|
||||
unlockDrawer()
|
||||
}
|
||||
restoreSelection(savedInstanceState)
|
||||
@@ -346,11 +349,60 @@ class DownloadsScene :
|
||||
val handlerDrawable = HandlerDrawable()
|
||||
handlerDrawable.setColor(theme.resolveColor(com.google.android.material.R.attr.colorPrimary))
|
||||
fastScroller.setHandlerDrawable(handlerDrawable)
|
||||
fabLayout.setExpanded(expanded = false, animation = false)
|
||||
fabLayout.addOnExpandListener { if (!it && tracker.isInCustomChoice) tracker.clearSelection() }
|
||||
fabLayout.setHidePrimaryFab(true)
|
||||
fabLayout.setAutoCancel(false)
|
||||
fabLayout.setOnClickFabListener(this@DownloadsScene)
|
||||
with(fabLayout) {
|
||||
autoCancel = false
|
||||
hide()
|
||||
addOnExpandStateListener { if (!it && tracker.isInCustomChoice) tracker.clearSelection() }
|
||||
secondaryFab = listOf(
|
||||
Icons.Default.DoneAll to {
|
||||
tracker.selectAll()
|
||||
throw CancellationException()
|
||||
},
|
||||
Icons.Default.PlayArrow to {
|
||||
val gidList = tracker.getAndClearSelection().mapToLongArray(DownloadInfo::gid)
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START_RANGE
|
||||
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
},
|
||||
Icons.Default.Pause to {
|
||||
val gidList = tracker.getAndClearSelection().mapToLongArray(DownloadInfo::gid)
|
||||
DownloadManager.stopRangeDownload(gidList)
|
||||
},
|
||||
Icons.Default.Delete to {
|
||||
val downloadInfoList = tracker.getAndClearSelection()
|
||||
val builder = CheckBoxDialogBuilder(
|
||||
context,
|
||||
getString(R.string.download_remove_dialog_message_2, downloadInfoList.size),
|
||||
getString(R.string.download_remove_dialog_check_text),
|
||||
Settings.removeImageFiles,
|
||||
)
|
||||
val helper = DeleteRangeDialogHelper(
|
||||
downloadInfoList,
|
||||
downloadInfoList.mapToLongArray(DownloadInfo::gid),
|
||||
builder,
|
||||
)
|
||||
builder.setTitle(R.string.download_remove_dialog_title)
|
||||
.setPositiveButton(android.R.string.ok, helper)
|
||||
.apply { withUIContext { show() } }
|
||||
},
|
||||
Icons.AutoMirrored.Default.DriveFileMove to {
|
||||
val downloadInfoList = tracker.getAndClearSelection()
|
||||
val labelRawList = DownloadManager.labelList
|
||||
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
|
||||
labelList.add(getString(R.string.default_download_label_name))
|
||||
labelRawList.forEach {
|
||||
labelList.add(it.label)
|
||||
}
|
||||
val labels = labelList.toTypedArray()
|
||||
val helper = MoveDialogHelper(labels, downloadInfoList)
|
||||
BaseDialogBuilder(context)
|
||||
.setTitle(R.string.download_move_dialog_title)
|
||||
.setItems(labels, helper)
|
||||
.apply { withUIContext { show() } }
|
||||
},
|
||||
)
|
||||
}
|
||||
updateForLabel()
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
@@ -531,80 +583,6 @@ class DownloadsScene :
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {
|
||||
if (tracker.isInCustomChoice) {
|
||||
tracker.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
|
||||
val context = context
|
||||
val activity: Activity? = mainActivity
|
||||
if (null == context || null == activity) {
|
||||
return
|
||||
}
|
||||
if (0 == position) {
|
||||
tracker.selectAll()
|
||||
} else {
|
||||
val downloadInfoList = tracker.getAndClearSelection()
|
||||
val gidList = if (position in 1..3) {
|
||||
downloadInfoList.mapToLongArray(DownloadInfo::gid)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
when (position) {
|
||||
1 -> {
|
||||
// Start
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START_RANGE
|
||||
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
// Stop
|
||||
lifecycleScope.launchIO {
|
||||
DownloadManager.stopRangeDownload(gidList!!)
|
||||
}
|
||||
}
|
||||
|
||||
3 -> {
|
||||
// Delete
|
||||
val builder = CheckBoxDialogBuilder(
|
||||
context,
|
||||
getString(R.string.download_remove_dialog_message_2, gidList!!.size),
|
||||
getString(R.string.download_remove_dialog_check_text),
|
||||
Settings.removeImageFiles,
|
||||
)
|
||||
val helper = DeleteRangeDialogHelper(
|
||||
downloadInfoList,
|
||||
gidList,
|
||||
builder,
|
||||
)
|
||||
builder.setTitle(R.string.download_remove_dialog_title)
|
||||
.setPositiveButton(android.R.string.ok, helper)
|
||||
.show()
|
||||
}
|
||||
|
||||
4 -> {
|
||||
// Move
|
||||
val labelRawList = DownloadManager.labelList
|
||||
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
|
||||
labelList.add(getString(R.string.default_download_label_name))
|
||||
labelRawList.forEach {
|
||||
labelList.add(it.label)
|
||||
}
|
||||
val labels = labelList.toTypedArray()
|
||||
val helper = MoveDialogHelper(labels, downloadInfoList)
|
||||
BaseDialogBuilder(context)
|
||||
.setTitle(R.string.download_move_dialog_title)
|
||||
.setItems(labels, helper)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.scene
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
@@ -24,6 +23,13 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
||||
import androidx.compose.material.icons.automirrored.filled.Redo
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.outlined.FolderSpecial
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -41,8 +47,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsAnimationCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -62,7 +66,6 @@ import com.google.android.material.datepicker.CompositeDateValidator
|
||||
import com.google.android.material.datepicker.DateValidatorPointBackward
|
||||
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -71,22 +74,17 @@ import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.databinding.SceneFavoritesBinding
|
||||
import com.hippo.ehviewer.ui.legacy.AddDeleteDrawable
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout.OnClickFabListener
|
||||
import com.hippo.ehviewer.ui.legacy.HandlerDrawable
|
||||
import com.hippo.ehviewer.ui.legacy.SecondaryFab
|
||||
import com.hippo.ehviewer.ui.legacy.ViewTransition
|
||||
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
|
||||
import com.hippo.ehviewer.ui.startDownload
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.SimpleHandler
|
||||
import com.hippo.ehviewer.util.getValue
|
||||
import com.hippo.ehviewer.util.lazyMut
|
||||
import com.hippo.ehviewer.util.mapToLongArray
|
||||
import com.hippo.ehviewer.util.setValue
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
@@ -94,7 +92,9 @@ import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -163,14 +163,7 @@ class FavoritesScene : SearchBarScene() {
|
||||
override val fastScroller get() = binding.fastScroller
|
||||
override val recyclerView get() = binding.recyclerView
|
||||
|
||||
private fun onItemClick(position: Int) {
|
||||
// Skip if in search mode
|
||||
if (!tracker.isInCustomChoice) {
|
||||
switchFav(position - 2)
|
||||
updateJumpFab()
|
||||
closeSideSheet()
|
||||
}
|
||||
}
|
||||
private var onDrawerItemClick: ((Int) -> Unit)? = null
|
||||
|
||||
private var collectJob: Job? = null
|
||||
|
||||
@@ -232,6 +225,87 @@ class FavoritesScene : SearchBarScene() {
|
||||
savedInstanceState: Bundle?,
|
||||
): ViewBinding {
|
||||
_binding = SceneFavoritesBinding.inflate(inflater, container)
|
||||
val fabActions = listOf<SecondaryFab>(
|
||||
// Normal
|
||||
Icons.AutoMirrored.Filled.Redo to {
|
||||
showGoToDialog()
|
||||
},
|
||||
Icons.Default.Refresh to {
|
||||
switchFav(urlBuilder.favCat)
|
||||
},
|
||||
Icons.Default.ArrowDownward to {
|
||||
urlBuilder.setIndex("1-0", false)
|
||||
mAdapter?.refresh()
|
||||
},
|
||||
|
||||
// Select
|
||||
Icons.Default.DoneAll to {
|
||||
tracker.selectAll()
|
||||
throw CancellationException()
|
||||
},
|
||||
Icons.Default.Download to {
|
||||
dialogState.startDownload(requireContext(), false, *takeCheckedInfo().toTypedArray())
|
||||
},
|
||||
Icons.Default.Delete to {
|
||||
dialogState.awaitPermissionOrCancel(title = R.string.delete_favorites_dialog_title) {
|
||||
Text(text = stringResource(R.string.delete_favorites_dialog_message, checkedSize()))
|
||||
}
|
||||
val info = takeCheckedInfo()
|
||||
val srcCat = urlBuilder.favCat
|
||||
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav
|
||||
EhDB.removeLocalFavorites(info)
|
||||
} else {
|
||||
val delList = info.mapToLongArray(BaseGalleryInfo::gid)
|
||||
EhEngine.modifyFavorites(delList, srcCat, -1)
|
||||
}
|
||||
mAdapter?.refresh()
|
||||
},
|
||||
Icons.AutoMirrored.Default.DriveFileMove to {
|
||||
// First is local favorite, the other 10 is cloud favorite
|
||||
val localFav = getString(R.string.local_favorites)
|
||||
val array = if (EhCookieStore.hasSignedIn()) {
|
||||
arrayOf(localFav, *Settings.favCat)
|
||||
} else {
|
||||
arrayOf(localFav)
|
||||
}
|
||||
val index = dialogState.showSelectItem(*array, title = R.string.move_favorites_dialog_title)
|
||||
val srcCat = urlBuilder.favCat
|
||||
val dstCat = if (index == 0) FavListUrlBuilder.FAV_CAT_LOCAL else index - 1
|
||||
val info = takeCheckedInfo()
|
||||
if (srcCat != dstCat) {
|
||||
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
|
||||
// Move from local to cloud
|
||||
EhDB.removeLocalFavorites(info)
|
||||
val galleryList = info.map { it.gid to it.token!! }
|
||||
runSuspendCatching {
|
||||
EhEngine.addFavorites(galleryList, dstCat)
|
||||
}
|
||||
} else if (dstCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
|
||||
// Move from cloud to local
|
||||
EhDB.putLocalFavorites(info)
|
||||
} else {
|
||||
// Move from cloud to cloud
|
||||
val gidArray = info.mapToLongArray(BaseGalleryInfo::gid)
|
||||
runSuspendCatching {
|
||||
EhEngine.modifyFavorites(gidArray, srcCat, dstCat)
|
||||
}
|
||||
}
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
},
|
||||
)
|
||||
fun updateJumpFab() {
|
||||
val isLocalFav = urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL
|
||||
binding.fabLayout.secondaryFab = if (isLocalFav) listOf(fabActions[1]) else fabActions.take(3)
|
||||
}
|
||||
onDrawerItemClick = {
|
||||
// Skip if in search mode
|
||||
if (!tracker.isInCustomChoice) {
|
||||
switchFav(it - 2)
|
||||
updateJumpFab()
|
||||
closeSideSheet()
|
||||
}
|
||||
}
|
||||
setOnApplySearch {
|
||||
if (!tracker.isInCustomChoice) {
|
||||
switchFav(urlBuilder.favCat, it)
|
||||
@@ -240,74 +314,9 @@ class FavoritesScene : SearchBarScene() {
|
||||
binding.fastScroller.attachToRecyclerView(binding.recyclerView)
|
||||
binding.fastScroller.setHandlerDrawable(HandlerDrawable().apply { setColor(inflater.context.theme.resolveColor(androidx.appcompat.R.attr.colorPrimary)) })
|
||||
binding.fabLayout.run {
|
||||
ViewCompat.setWindowInsetsAnimationCallback(binding.root, WindowInsetsAnimationHelper(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP, this))
|
||||
updateJumpFab()
|
||||
val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface)
|
||||
val addDelete = AddDeleteDrawable(context, colorID)
|
||||
primaryFab.setImageDrawable(addDelete)
|
||||
setExpanded(expanded = false, animation = false)
|
||||
setAutoCancel(true)
|
||||
setHidePrimaryFab(false)
|
||||
setOnClickFabListener(object : OnClickFabListener {
|
||||
override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {
|
||||
if (tracker.isInCustomChoice) {
|
||||
tracker.clearSelection()
|
||||
} else {
|
||||
binding.fabLayout.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
|
||||
if (!tracker.isInCustomChoice) {
|
||||
when (position) {
|
||||
0 -> showGoToDialog()
|
||||
1 -> switchFav(urlBuilder.favCat)
|
||||
2 -> {
|
||||
urlBuilder.setIndex("1-0", false)
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
}
|
||||
view.isExpanded = false
|
||||
return
|
||||
}
|
||||
when (position) {
|
||||
// Check all
|
||||
3 -> tracker.selectAll()
|
||||
// Download
|
||||
4 -> lifecycleScope.launchUI {
|
||||
dialogState.startDownload(context, false, *takeCheckedInfo().toTypedArray())
|
||||
}
|
||||
// Delete
|
||||
5 -> {
|
||||
val helper = DeleteDialogHelper()
|
||||
BaseDialogBuilder(context)
|
||||
.setTitle(R.string.delete_favorites_dialog_title)
|
||||
.setMessage(getString(R.string.delete_favorites_dialog_message, checkedSize()))
|
||||
.setPositiveButton(android.R.string.ok, helper)
|
||||
.show()
|
||||
}
|
||||
// Move
|
||||
6 -> {
|
||||
val helper = MoveDialogHelper()
|
||||
// First is local favorite, the other 10 is cloud favorite
|
||||
val localFav = getString(R.string.local_favorites)
|
||||
val array = if (EhCookieStore.hasSignedIn()) {
|
||||
arrayOf(localFav, *Settings.favCat)
|
||||
} else {
|
||||
arrayOf(localFav)
|
||||
}
|
||||
BaseDialogBuilder(context).setTitle(R.string.move_favorites_dialog_title).setItems(array, helper).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
addOnExpandListener {
|
||||
if (it) {
|
||||
addDelete.setDelete(FabLayout.ANIMATE_TIME)
|
||||
} else {
|
||||
addDelete.setAdd(FabLayout.ANIMATE_TIME)
|
||||
if (tracker.isInCustomChoice) tracker.clearSelection()
|
||||
}
|
||||
addOnExpandStateListener {
|
||||
if (tracker.isInCustomChoice && !it) tracker.clearSelection()
|
||||
}
|
||||
}
|
||||
binding.recyclerView.run {
|
||||
@@ -360,20 +369,27 @@ class FavoritesScene : SearchBarScene() {
|
||||
{ adapter.snapshot().items },
|
||||
{ (this as? GalleryHolder)?.galleryId },
|
||||
).apply {
|
||||
addCustomChoiceListener({
|
||||
showSelectionFab()
|
||||
binding.fabLayout.setAutoCancel(false)
|
||||
// Delay expanding action to make layout work fine
|
||||
SimpleHandler.post { binding.fabLayout.isExpanded = true }
|
||||
binding.refreshLayout.isEnabled = false
|
||||
lockDrawer()
|
||||
}) {
|
||||
showNormalFab()
|
||||
binding.fabLayout.setAutoCancel(true)
|
||||
binding.fabLayout.isExpanded = false
|
||||
binding.refreshLayout.isEnabled = true
|
||||
unlockDrawer()
|
||||
}
|
||||
addCustomChoiceListener(
|
||||
onIntoCustomChoiceListener = {
|
||||
showNormalFabsJob?.cancel()
|
||||
binding.fabLayout.secondaryFab = fabActions.drop(3)
|
||||
binding.fabLayout.autoCancel = false
|
||||
binding.fabLayout.expanded = true
|
||||
binding.refreshLayout.isEnabled = false
|
||||
lockDrawer()
|
||||
},
|
||||
onOutOfCustomChoiceListener = {
|
||||
// Delay showing normal fab to avoid mutation
|
||||
showNormalFabsJob = lifecycleScope.launch {
|
||||
delay(300)
|
||||
updateJumpFab()
|
||||
}
|
||||
binding.fabLayout.autoCancel = true
|
||||
binding.fabLayout.expanded = false
|
||||
binding.refreshLayout.isEnabled = true
|
||||
unlockDrawer()
|
||||
},
|
||||
)
|
||||
restoreSelection(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -415,24 +431,13 @@ class FavoritesScene : SearchBarScene() {
|
||||
return binding
|
||||
}
|
||||
|
||||
// Hide jump fab on local fav cat
|
||||
private fun updateJumpFab() {
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(
|
||||
0,
|
||||
urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,
|
||||
)
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(
|
||||
2,
|
||||
urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRelease() {
|
||||
super.onRelease()
|
||||
binding.recyclerView.stopScroll()
|
||||
mAdapter = null
|
||||
_binding = null
|
||||
showNormalFabsJob = null
|
||||
onDrawerItemClick = null
|
||||
}
|
||||
|
||||
override fun onCreateDrawerView(inflater: LayoutInflater) = ComposeWithMD3 {
|
||||
@@ -460,7 +465,7 @@ class FavoritesScene : SearchBarScene() {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = name) },
|
||||
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
|
||||
modifier = Modifier.clickable { onItemClick(index) },
|
||||
modifier = Modifier.clickable { onDrawerItemClick?.invoke(index) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -474,7 +479,11 @@ class FavoritesScene : SearchBarScene() {
|
||||
|
||||
override fun onSearchViewHidden() {
|
||||
super.onSearchViewHidden()
|
||||
binding.fabLayout.show(delay = SEARCH_VIEW_ANIMATE_TIME)
|
||||
lifecycleScope.launch {
|
||||
delay(300)
|
||||
_binding ?: return@launch
|
||||
binding.fabLayout.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGoToDialog() {
|
||||
@@ -506,79 +515,4 @@ class FavoritesScene : SearchBarScene() {
|
||||
private fun takeCheckedInfo() = tracker.getAndClearSelection()
|
||||
|
||||
private fun checkedSize() = tracker.selectionSize
|
||||
|
||||
private fun showNormalFab() {
|
||||
// Delay showing normal fab to avoid mutation
|
||||
showNormalFabsJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||
delay(300)
|
||||
updateJumpFab() // index: 0, 2
|
||||
binding.fabLayout.run {
|
||||
setSecondaryFabVisibilityAt(1, true)
|
||||
for (i in 3..6) {
|
||||
setSecondaryFabVisibilityAt(i, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSelectionFab() {
|
||||
showNormalFabsJob?.cancel()
|
||||
binding.fabLayout.run {
|
||||
for (i in 0..2) {
|
||||
setSecondaryFabVisibilityAt(i, false)
|
||||
}
|
||||
for (i in 3..6) {
|
||||
setSecondaryFabVisibilityAt(i, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DeleteDialogHelper : DialogInterface.OnClickListener {
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
val info = takeCheckedInfo()
|
||||
lifecycleScope.launchIO {
|
||||
val srcCat = urlBuilder.favCat
|
||||
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav
|
||||
EhDB.removeLocalFavorites(info)
|
||||
} else {
|
||||
val delList = info.map { it.gid }.toLongArray()
|
||||
EhEngine.modifyFavorites(delList, srcCat, -1)
|
||||
}
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MoveDialogHelper : DialogInterface.OnClickListener {
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
val srcCat = urlBuilder.favCat
|
||||
val dstCat = if (which == 0) {
|
||||
FavListUrlBuilder.FAV_CAT_LOCAL
|
||||
} else {
|
||||
which - 1
|
||||
}
|
||||
if (srcCat == dstCat) return
|
||||
val info = takeCheckedInfo()
|
||||
lifecycleScope.launchIO {
|
||||
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
|
||||
// Move from local to cloud
|
||||
EhDB.removeLocalFavorites(info)
|
||||
val galleryList = info.map { it.gid to it.token!! }
|
||||
runCatching {
|
||||
EhEngine.addFavorites(galleryList, dstCat)
|
||||
}
|
||||
} else if (dstCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
|
||||
// Move from cloud to local
|
||||
EhDB.putLocalFavorites(info)
|
||||
} else {
|
||||
// Move from cloud to cloud
|
||||
val gidArray = info.map { it.gid }.toLongArray()
|
||||
runCatching {
|
||||
EhEngine.modifyFavorites(gidArray, srcCat, dstCat)
|
||||
}
|
||||
}
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@ import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LastPage
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.outlined.Bookmarks
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -58,7 +60,6 @@ import com.google.android.material.datepicker.CompositeDateValidator
|
||||
import com.google.android.material.datepicker.DateValidatorPointBackward
|
||||
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -82,16 +83,17 @@ import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.databinding.DrawerListRvBinding
|
||||
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
|
||||
import com.hippo.ehviewer.databinding.SceneGalleryListBinding
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.icons.filled.GoTo
|
||||
import com.hippo.ehviewer.ui.WebViewActivity
|
||||
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
||||
import com.hippo.ehviewer.ui.legacy.AddDeleteDrawable
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.BringOutTransition
|
||||
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout.OnClickFabListener
|
||||
import com.hippo.ehviewer.ui.legacy.FAB_ANIMATE_TIME
|
||||
import com.hippo.ehviewer.ui.legacy.HandlerDrawable
|
||||
import com.hippo.ehviewer.ui.legacy.LayoutManagerUtils.firstVisibleItemPosition
|
||||
import com.hippo.ehviewer.ui.legacy.SecondaryFab
|
||||
import com.hippo.ehviewer.ui.legacy.ViewTransition
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
@@ -249,13 +251,8 @@ class GalleryListScene : SearchBarScene() {
|
||||
var keyword = mUrlBuilder.keyword
|
||||
val category = mUrlBuilder.category
|
||||
val mode = mUrlBuilder.mode
|
||||
val isPopular = mode == MODE_WHATS_HOT
|
||||
mIsTopList = mode == MODE_TOPLIST
|
||||
|
||||
// Update fab visibility
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(0, !isPopular)
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(2, !isPopular)
|
||||
|
||||
// Update normal search mode and category
|
||||
binding.searchLayout.setSearchMyTags(mode == MODE_SUBSCRIPTION)
|
||||
if (category != EhUtils.NONE) {
|
||||
@@ -388,49 +385,40 @@ class GalleryListScene : SearchBarScene() {
|
||||
GalleryPageUrlSuggestion(gid, pToken, page)
|
||||
}
|
||||
}
|
||||
binding.fabLayout.setAutoCancel(true)
|
||||
binding.fabLayout.isExpanded = false
|
||||
binding.fabLayout.setHidePrimaryFab(false)
|
||||
binding.fabLayout.setOnClickFabListener(object : OnClickFabListener {
|
||||
override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {
|
||||
if (State.NORMAL == mState) {
|
||||
view.toggle()
|
||||
}
|
||||
}
|
||||
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
|
||||
when (position) {
|
||||
0 -> showGoToDialog()
|
||||
1 -> { // First
|
||||
mUrlBuilder.setIndex(null, true)
|
||||
mUrlBuilder.mJumpTo = null
|
||||
val isPop = mUrlBuilder.mode == MODE_WHATS_HOT
|
||||
binding.fabLayout.apply {
|
||||
val fab = listOf<SecondaryFab>(
|
||||
Icons.Default.Refresh to {
|
||||
mUrlBuilder.setIndex(null, true)
|
||||
mUrlBuilder.mJumpTo = null
|
||||
mAdapter?.refresh()
|
||||
},
|
||||
EhIcons.Default.GoTo to {
|
||||
showGoToDialog()
|
||||
},
|
||||
Icons.AutoMirrored.Default.LastPage to {
|
||||
if (mIsTopList) {
|
||||
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
|
||||
mAdapter?.refresh()
|
||||
} else {
|
||||
mUrlBuilder.setIndex("1", false)
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
|
||||
2 -> { // Last
|
||||
if (mIsTopList) {
|
||||
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
|
||||
mAdapter?.refresh()
|
||||
} else {
|
||||
mUrlBuilder.setIndex("1", false)
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
view.isExpanded = false
|
||||
}
|
||||
})
|
||||
val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface)
|
||||
val actionFabDrawable = AddDeleteDrawable(requireContext(), colorID)
|
||||
binding.fabLayout.addOnExpandListener {
|
||||
if (it) {
|
||||
lockDrawer()
|
||||
actionFabDrawable.setDelete(FabLayout.ANIMATE_TIME)
|
||||
},
|
||||
)
|
||||
secondaryFab = if (!isPop) {
|
||||
fab
|
||||
} else {
|
||||
fab.take(1)
|
||||
}
|
||||
}
|
||||
binding.fabLayout.addOnExpandStateListener {
|
||||
if (it) {
|
||||
lockDrawer()
|
||||
} else {
|
||||
unlockDrawer()
|
||||
actionFabDrawable.setAdd(FabLayout.ANIMATE_TIME)
|
||||
}
|
||||
}
|
||||
binding.fabLayout.primaryFab.setImageDrawable(actionFabDrawable)
|
||||
|
||||
// Update list url builder
|
||||
onUpdateUrlBuilder()
|
||||
@@ -646,20 +634,28 @@ class GalleryListScene : SearchBarScene() {
|
||||
}
|
||||
|
||||
private fun showSearchFab(delay: Long) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
lifecycleScope.launch {
|
||||
delay(delay)
|
||||
showSearchFab = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectSearchFab(animation: Boolean) {
|
||||
val delay = binding.fabLayout.hide(animation)
|
||||
showSearchFab(delay)
|
||||
private fun showPrimaryFab(delay: Long) {
|
||||
lifecycleScope.launch {
|
||||
delay(delay)
|
||||
_binding ?: return@launch
|
||||
binding.fabLayout.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectActionFab(animation: Boolean) {
|
||||
private fun selectSearchFab() {
|
||||
binding.fabLayout.hide()
|
||||
showSearchFab(FAB_ANIMATE_TIME.toLong())
|
||||
}
|
||||
|
||||
private fun selectActionFab() {
|
||||
showSearchFab = false
|
||||
binding.fabLayout.show(animation, FabLayout.ANIMATE_TIME)
|
||||
showPrimaryFab(FAB_ANIMATE_TIME.toLong())
|
||||
}
|
||||
|
||||
override fun onSearchViewExpanded() {
|
||||
@@ -675,7 +671,7 @@ class GalleryListScene : SearchBarScene() {
|
||||
override fun onSearchViewHidden() {
|
||||
super.onSearchViewHidden()
|
||||
if (mState == State.NORMAL) {
|
||||
binding.fabLayout.show(delay = SEARCH_VIEW_ANIMATE_TIME)
|
||||
showPrimaryFab(SEARCH_VIEW_ANIMATE_TIME)
|
||||
} else {
|
||||
showSearchFab(SEARCH_VIEW_ANIMATE_TIME)
|
||||
stateBackPressedCallback.isEnabled = true
|
||||
@@ -708,27 +704,27 @@ class GalleryListScene : SearchBarScene() {
|
||||
when (oldState) {
|
||||
State.NORMAL -> when (state) {
|
||||
State.SIMPLE_SEARCH -> {
|
||||
selectSearchFab(animation)
|
||||
selectSearchFab()
|
||||
}
|
||||
State.SEARCH -> {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
selectSearchFab(animation)
|
||||
selectSearchFab()
|
||||
}
|
||||
State.SEARCH_SHOW_LIST -> {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
selectSearchFab(animation)
|
||||
selectSearchFab()
|
||||
}
|
||||
else -> error("Unreachable!!!")
|
||||
}
|
||||
State.SIMPLE_SEARCH -> when (state) {
|
||||
State.NORMAL -> selectActionFab(animation)
|
||||
State.NORMAL -> selectActionFab()
|
||||
State.SEARCH -> mViewTransition!!.showView(1, animation)
|
||||
State.SEARCH_SHOW_LIST -> mViewTransition!!.showView(1, animation)
|
||||
else -> error("Unreachable!!!")
|
||||
}
|
||||
State.SEARCH, State.SEARCH_SHOW_LIST -> if (state == State.NORMAL) {
|
||||
mViewTransition!!.showView(0, animation)
|
||||
selectActionFab(animation)
|
||||
selectActionFab()
|
||||
} else if (state == State.SIMPLE_SEARCH) {
|
||||
mViewTransition!!.showView(0, animation)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -52,6 +51,8 @@ import androidx.compose.runtime.rememberCompositionContext
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
@@ -61,6 +62,7 @@ import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.platform.WindowInfo
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.viewinterop.AndroidViewBinding
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
@@ -79,6 +81,7 @@ import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.dao.Search
|
||||
import com.hippo.ehviewer.dao.SearchDao
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
import com.hippo.ehviewer.ui.legacy.FAB_ANIMATE_TIME
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.util.findActivity
|
||||
import com.hippo.ehviewer.util.getSparseParcelableArrayCompat
|
||||
@@ -310,14 +313,16 @@ fun SearchBarScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = showSearchFab,
|
||||
enter = scaleIn(),
|
||||
exit = scaleOut(),
|
||||
val hiddenState by animateFloatAsState(
|
||||
targetValue = if (showSearchFab) 1f else 0f,
|
||||
animationSpec = tween(FAB_ANIMATE_TIME),
|
||||
label = "hiddenState",
|
||||
)
|
||||
FloatingActionButton(
|
||||
onClick = ::onApplySearch,
|
||||
modifier = Modifier.rotate(lerp(90f, 0f, hiddenState)).scale(hiddenState),
|
||||
) {
|
||||
FloatingActionButton(onClick = ::onApplySearch) {
|
||||
Icon(imageVector = Icons.Default.Search, contentDescription = null)
|
||||
}
|
||||
Icon(imageVector = Icons.Default.Search, contentDescription = null)
|
||||
}
|
||||
},
|
||||
content = content,
|
||||
@@ -353,7 +358,6 @@ abstract class SearchBarScene : BaseScene() {
|
||||
return ComposeWithMD3 {
|
||||
val compositionContext = rememberCompositionContext()
|
||||
val density = LocalDensity.current
|
||||
val fabPadding = with(density) { 16.dp.roundToPx() }
|
||||
val margin = with(density) { 8.dp.roundToPx() }
|
||||
// Workaround for BTF2 cursor not showing
|
||||
// https://issuetracker.google.com/issues/307323842
|
||||
@@ -397,7 +401,7 @@ abstract class SearchBarScene : BaseScene() {
|
||||
onRelease()
|
||||
},
|
||||
) {
|
||||
fabLayout.updatePadding(bottom = fabPadding + bottomPadding)
|
||||
fabLayout.updatePadding(bottom = bottomPadding)
|
||||
fastScroller.updatePadding(top = topPadding + margin, bottom = bottomPadding)
|
||||
recyclerView.updatePadding(top = topPadding + margin, bottom = bottomPadding)
|
||||
}
|
||||
|
||||
@@ -1,27 +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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_check_all" />
|
||||
|
||||
</vector>
|
||||
@@ -1,27 +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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_close" />
|
||||
|
||||
</vector>
|
||||
@@ -1,27 +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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_delete" />
|
||||
|
||||
</vector>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2019 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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_download" />
|
||||
|
||||
</vector>
|
||||
@@ -1,27 +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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_folder_move" />
|
||||
|
||||
</vector>
|
||||
@@ -1,28 +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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_go_to" />
|
||||
|
||||
</vector>
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2022 Tarsin Norbin
|
||||
~
|
||||
~ This file is part of EhViewer
|
||||
~
|
||||
~ EhViewer is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU General Public License as published
|
||||
~ by the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ EhViewer is distributed in the hope that it will be useful, but WITHOUT
|
||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
~ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,18 L5.6,16.6 10.2,12 5.6,7.4 7,6 13,12ZM16,18V6H18V18Z" />
|
||||
</vector>
|
||||
@@ -23,7 +23,7 @@
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<com.hippo.ehviewer.ui.legacy.IgnoreCancelRV
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -55,45 +55,6 @@
|
||||
<com.hippo.ehviewer.ui.legacy.FabLayout
|
||||
android:id="@+id/fab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingEnd="@dimen/corner_fab_margin"
|
||||
android:paddingBottom="@dimen/corner_fab_margin">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_check_all_dark_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_play_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_pause_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_delete_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_folder_move_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_close_dark_x24" />
|
||||
|
||||
</com.hippo.ehviewer.ui.legacy.FabLayout>
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</merge>
|
||||
@@ -14,8 +14,7 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<include
|
||||
android:id="@+id/content_layout"
|
||||
@@ -24,59 +23,5 @@
|
||||
<com.hippo.ehviewer.ui.legacy.FabLayout
|
||||
android:id="@+id/fab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingEnd="@dimen/corner_fab_margin"
|
||||
android:paddingBottom="@dimen/corner_fab_margin">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_go_to_dark_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_refresh_dark_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_last_page_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/v_check_all_dark_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/v_download_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/v_delete_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/v_folder_move_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.hippo.ehviewer.ui.legacy.FabLayout>
|
||||
android:layout_height="match_parent" />
|
||||
</merge>
|
||||
@@ -14,8 +14,7 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<include
|
||||
android:id="@+id/content_layout"
|
||||
@@ -31,32 +30,6 @@
|
||||
<com.hippo.ehviewer.ui.legacy.FabLayout
|
||||
android:id="@+id/fab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingEnd="@dimen/corner_fab_margin"
|
||||
android:paddingBottom="@dimen/corner_fab_margin">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_go_to_dark_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_refresh_dark_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/v_last_page_x24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.hippo.ehviewer.ui.legacy.FabLayout>
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</merge>
|
||||
@@ -42,7 +42,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<com.hippo.ehviewer.ui.legacy.IgnoreCancelRV
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -60,14 +60,6 @@
|
||||
|
||||
<dimen name="strip_item_padding_v">@dimen/strip_item_padding</dimen>
|
||||
|
||||
<!-- FabLayout -->
|
||||
<dimen name="fab_layout_primary_margin">24dp</dimen>
|
||||
<dimen name="fab_layout_secondary_margin">16dp</dimen>
|
||||
<dimen name="fab_size">56dp</dimen>
|
||||
<dimen name="fab_min_size">40dp</dimen>
|
||||
|
||||
<dimen name="corner_fab_margin">16dp</dimen>
|
||||
|
||||
<dimen name="gallery_detail_thumb_width">128dp</dimen>
|
||||
<dimen name="gallery_detail_thumb_height">192dp</dimen>
|
||||
|
||||
|
||||
@@ -16,15 +16,9 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="pd_close" translatable="false">M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z</string>
|
||||
<string name="pd_pause" translatable="false">M14,19.14H18V5.14H14M6,19.14H10V5.14H6V19.14Z</string>
|
||||
<string name="pd_play" translatable="false">M8,5.14V19.14L19,12.14L8,5.14Z</string>
|
||||
<string name="pd_check_all" translatable="false">M0.41,13.41L6,19L7.41,17.58L1.83,12M22.24,5.58L11.66,16.17L7.5,12L6.07,13.41L11.66,19L23.66,7M18,7L16.59,5.58L10.24,11.93L11.66,13.34L18,7Z</string>
|
||||
<string name="pd_folder_move" translatable="false">M9,18V15H5V11H9V8L14,13M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z</string>
|
||||
<string name="pd_plus" translatable="false">M21,13H13V21H11V13H3V11H11V3H13V11H21V13Z</string>
|
||||
<string name="pd_download" translatable="false">M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z</string>
|
||||
<string name="pd_delete" translatable="false">M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z</string>
|
||||
<string name="pd_go_to" translatable="false">M13,13L17,17L21,13H18V10C18,6.1,14.9,3,11,3S4,6.1,4,10V17H6V10C6,7.2,8.2,5,11,5S16,7.2,16,10V13H13M,16,19H18V21H16V19Z</string>
|
||||
<string name="pd_help_circle" translatable="false">M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z</string>
|
||||
<string name="pd_download_box" translatable="false">M19,3H5C3.9,3,3,3.9,3,5V19C3,20.1,3.9,21,5,21H19C20.1,21,21,20.1,21,19V5C21,3.9,20.1,3,19,3ZM12,17L7,12H10V8H14V12H17L12,17Z</string>
|
||||
<string name="pd_refresh" translatable="false">M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z</string>
|
||||
|
||||
Reference in new issue
Block a user