diff --git a/app/src/main/kotlin/com/hippo/ehviewer/icons/filled/GoTo.kt b/app/src/main/kotlin/com/hippo/ehviewer/icons/filled/GoTo.kt new file mode 100644 index 000000000..9a62ad99f --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/icons/filled/GoTo.kt @@ -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() + } + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FabLayout.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FabLayout.kt index 8eb398f81..dc1037955 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FabLayout.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FabLayout.kt @@ -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 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() - 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?>(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() - 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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/IgnoreCancelRV.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/IgnoreCancelRV.kt new file mode 100644 index 000000000..0e840d39b --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/IgnoreCancelRV.kt @@ -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) + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsScene.kt index aaf33736a..12f401910 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsScene.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsScene.kt @@ -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 = 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 = 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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesScene.kt index b68b44525..aaa9284b6 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesScene.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesScene.kt @@ -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( + // 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() - } - } - } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListScene.kt index 54faccca0..09761160a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListScene.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListScene.kt @@ -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( + 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) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt index 906fb9a7d..962418c75 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt @@ -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) } diff --git a/app/src/main/res/drawable/v_check_all_dark_x24.xml b/app/src/main/res/drawable/v_check_all_dark_x24.xml deleted file mode 100644 index 73c6892f7..000000000 --- a/app/src/main/res/drawable/v_check_all_dark_x24.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/v_close_dark_x24.xml b/app/src/main/res/drawable/v_close_dark_x24.xml deleted file mode 100644 index 325305c5e..000000000 --- a/app/src/main/res/drawable/v_close_dark_x24.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/v_delete_x24.xml b/app/src/main/res/drawable/v_delete_x24.xml deleted file mode 100644 index 1de3b4576..000000000 --- a/app/src/main/res/drawable/v_delete_x24.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/v_download_x24.xml b/app/src/main/res/drawable/v_download_x24.xml deleted file mode 100644 index b00291136..000000000 --- a/app/src/main/res/drawable/v_download_x24.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/v_folder_move_x24.xml b/app/src/main/res/drawable/v_folder_move_x24.xml deleted file mode 100644 index fa385adee..000000000 --- a/app/src/main/res/drawable/v_folder_move_x24.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/v_go_to_dark_x24.xml b/app/src/main/res/drawable/v_go_to_dark_x24.xml deleted file mode 100644 index e45b38e79..000000000 --- a/app/src/main/res/drawable/v_go_to_dark_x24.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/v_last_page_x24.xml b/app/src/main/res/drawable/v_last_page_x24.xml deleted file mode 100644 index da707bbdb..000000000 --- a/app/src/main/res/drawable/v_last_page_x24.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/scene_download.xml b/app/src/main/res/layout/scene_download.xml index 4177f0afa..6534c42d2 100644 --- a/app/src/main/res/layout/scene_download.xml +++ b/app/src/main/res/layout/scene_download.xml @@ -23,7 +23,7 @@ android:layout_height="match_parent" app:layout_behavior="@string/searchbar_scrolling_view_behavior"> - - - - - - - - - - - - - - - + android:layout_height="match_parent" /> diff --git a/app/src/main/res/layout/scene_favorites.xml b/app/src/main/res/layout/scene_favorites.xml index 2ae230fa8..56bb6cbd7 100644 --- a/app/src/main/res/layout/scene_favorites.xml +++ b/app/src/main/res/layout/scene_favorites.xml @@ -14,8 +14,7 @@ ~ limitations under the License. --> - + - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" /> diff --git a/app/src/main/res/layout/scene_gallery_list.xml b/app/src/main/res/layout/scene_gallery_list.xml index b21f6a366..5f753a72a 100644 --- a/app/src/main/res/layout/scene_gallery_list.xml +++ b/app/src/main/res/layout/scene_gallery_list.xml @@ -14,8 +14,7 @@ ~ limitations under the License. --> - + - - - - - - - - - - + android:layout_height="match_parent" /> diff --git a/app/src/main/res/layout/widget_content_layout.xml b/app/src/main/res/layout/widget_content_layout.xml index 55d0128e7..a84e809b5 100644 --- a/app/src/main/res/layout/widget_content_layout.xml +++ b/app/src/main/res/layout/widget_content_layout.xml @@ -42,7 +42,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @dimen/strip_item_padding - - 24dp - 16dp - 56dp - 40dp - - 16dp - 128dp 192dp diff --git a/app/src/main/res/values/pathdata.xml b/app/src/main/res/values/pathdata.xml index bcf1e003b..c65d96b28 100644 --- a/app/src/main/res/values/pathdata.xml +++ b/app/src/main/res/values/pathdata.xml @@ -16,15 +16,9 @@ - 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 M14,19.14H18V5.14H14M6,19.14H10V5.14H6V19.14Z M8,5.14V19.14L19,12.14L8,5.14Z - 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 - 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 M21,13H13V21H11V13H3V11H11V3H13V11H21V13Z - M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z - M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z - 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 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 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 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