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:
authored and GitHub committed 2023-11-19 09:21:52 +08:00
1 parent 8be6ad84fc
commit e69eba6023
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>
+2 -41
View File
@@ -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>
+2 -57
View File
@@ -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>
+2 -29
View File
@@ -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"
-8
View File
@@ -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>
-6
View File
@@ -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>