Compose DownloadScreen (#343)

* Initial Compose DownloadScreen

* Cleanup namespace

* Reorderable

* Reorderable

* Fixes

* Fix layout

* Fixes

* State labelList

* Fixes

* SwipeToDismiss

* RenameLabel

* Rewrite renameLabel to notify

* Fix name shadow

* Fixes

* Fixes

* SelectMode

* Drop unused

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2023-11-26 20:12:01 +08:00
1 parent 6bd8063976
commit 2a667e62f4
28 files changed
+604 -2072

No files matched your search

@@ -1,33 +0,0 @@
package androidx.swiperefreshlayout.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.ShapeDrawable
import android.os.Parcelable
import android.util.AttributeSet
import androidx.core.graphics.ColorUtils
import eu.kanade.tachiyomi.util.system.dpToPx
import rikka.core.res.resolveColor
@SuppressLint("PrivateResource")
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : SwipeRefreshLayout(context, attrs) {
init {
val surfaceColor =
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
val overlayColor =
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
setProgressViewOffset(true, 64.dpToPx, 128.dpToPx)
}
override fun onSaveInstanceState(): Parcelable? {
isRefreshing = false
return super.onSaveInstanceState()
}
}
@@ -23,6 +23,7 @@ import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import com.google.android.material.math.MathUtils
import com.hippo.ehviewer.EhDB
@@ -44,6 +45,7 @@ import com.hippo.ehviewer.util.mapNotNull
import com.hippo.ehviewer.util.runAssertingNotMainThread
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import java.util.LinkedList
import kotlinx.coroutines.CoroutineScope
@@ -70,7 +72,9 @@ object DownloadManager : OnSpiderListener {
private val map: MutableMap<String?, LinkedList<DownloadInfo>>
// All labels without default label
val labelList = runAssertingNotMainThread { EhDB.getAllDownloadLabelList() } as MutableList<DownloadLabel>
val labelList = mutableStateListOf<DownloadLabel>().apply {
addAll(runAssertingNotMainThread { EhDB.getAllDownloadLabelList() })
}
// Store download info with default label
val defaultInfoList: LinkedList<DownloadInfo>
@@ -595,7 +599,7 @@ object DownloadManager : OnSpiderListener {
/**
* @param label Not allow new label
*/
suspend fun changeLabel(list: List<DownloadInfo>, label: String?) {
suspend fun changeLabel(list: Collection<DownloadInfo>, label: String?) {
if (null != label && !containLabel(label)) {
Log.e(TAG, "Not exits label: $label")
return
@@ -636,35 +640,24 @@ object DownloadManager : OnSpiderListener {
suspend fun moveLabel(fromPosition: Int, toPosition: Int) {
val item = labelList.removeAt(fromPosition)
labelList.add(toPosition, item)
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
val list = labelList.slice(range)
list.zip(range).forEach { it.first.position = it.second }
EhDB.updateDownloadLabel(list)
withIOContext {
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
val list = labelList.slice(range)
list.zip(range).forEach { it.first.position = it.second }
EhDB.updateDownloadLabel(list)
}
}
suspend fun renameLabel(from: String, to: String) {
// Find in label list
var found = false
for (raw in labelList) {
if (from == raw.label) {
found = true
raw.label = to
// Update in DB
EhDB.updateDownloadLabel(raw)
break
}
val index = labelList.indexOfFirst { it.label == from }
if (index != -1) {
val exist = labelList.removeAt(index)
val new = exist.copy(label = to)
labelList.add(index, new)
EhDB.updateDownloadLabel(new)
val list = map.remove(from)?.onEach { it.label = to }
if (list != null) map[to] = list
}
if (!found) {
return
}
val list = map.remove(from) ?: return
// Update info label
for (info in list) {
info.label = to
}
// Put list back with new label
map[to] = list
}
suspend fun deleteLabel(label: String) {
@@ -35,7 +35,7 @@ import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.DownloadsScene
import com.hippo.ehviewer.ui.scene.DownloadsFragment
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.SimpleHandler
@@ -187,7 +187,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
clearIntent.action = ACTION_CLEAR
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
val bundle = Bundle()
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE)
bundle.putString(DownloadsFragment.KEY_ACTION, DownloadsFragment.ACTION_CLEAR_DOWNLOAD_SERVICE)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = ACTION_START_DOWNLOADSCENE
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
@@ -243,7 +243,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
}
ensureDownloadingBuilder()
val bundle = Bundle()
bundle.putLong(DownloadsScene.KEY_GID, info.gid)
bundle.putLong(DownloadsFragment.KEY_GID, info.gid)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = ACTION_START_DOWNLOADSCENE
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
@@ -353,7 +353,7 @@ suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () -
}
}
suspend fun DialogState.confirmRemoveDownloadRange(list: List<DownloadInfo>) {
suspend fun DialogState.confirmRemoveDownloadRange(list: Collection<DownloadInfo>) {
var checked by mutableStateOf(Settings.removeImageFiles)
awaitPermissionOrCancel(
title = R.string.download_remove_dialog_title,
@@ -401,3 +401,11 @@ suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
val label = if (selected == 0) null else labels[selected - 1]
withUIContext { DownloadManager.changeLabel(listOf(downloadInfo), label) }
}
suspend fun DialogState.showMoveDownloadLabelList(list: Collection<DownloadInfo>) {
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
val labels = DownloadManager.labelList.map { it.label }.toTypedArray()
val selected = showSelectItem(defaultLabel, *labels, title = R.string.download_move_dialog_title)
val label = if (selected == 0) null else labels[selected - 1]
withUIContext { DownloadManager.changeLabel(list, label) }
}
@@ -104,7 +104,7 @@ import com.hippo.ehviewer.image.Image.Companion.decodeBitmap
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
import com.hippo.ehviewer.ui.scene.GalleryDetailFragment
import com.hippo.ehviewer.ui.scene.GalleryListFragment.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.ProgressFragment
import com.hippo.ehviewer.ui.scene.TokenArgs
@@ -507,7 +507,7 @@ class MainActivity : EhActivity() {
launch = {
navController.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to TokenArgs(result1.gid, result1.token)),
bundleOf(GalleryDetailFragment.KEY_ARGS to TokenArgs(result1.gid, result1.token)),
)
}
}
@@ -1,48 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
class AutoStaggeredGridLayoutManager(columnSize: Int, orientation: Int) : StaggeredGridLayoutManager(1, orientation) {
private var mColumnSize = columnSize
private var mColumnSizeChanged = true
fun setColumnSize(columnSize: Int) {
if (columnSize == mColumnSize) {
return
}
mColumnSize = columnSize
mColumnSizeChanged = true
}
override fun supportsPredictiveItemAnimations() = false
override fun onMeasure(recycler: RecyclerView.Recycler, state: RecyclerView.State, widthSpec: Int, heightSpec: Int) {
if (mColumnSizeChanged && mColumnSize > 0) {
val totalSpace = if (orientation == VERTICAL) {
View.MeasureSpec.getSize(widthSpec) - paddingRight - paddingLeft
} else {
View.MeasureSpec.getSize(heightSpec) - paddingTop - paddingBottom
}
spanCount = (totalSpace / mColumnSize).coerceAtLeast(1)
mColumnSizeChanged = false
}
super.onMeasure(recycler, state, widthSpec, heightSpec)
}
}
@@ -1,158 +0,0 @@
package com.hippo.ehviewer.ui.legacy
import android.content.Context
import android.util.AttributeSet
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.Close
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.alpha
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,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
var secondaryFab by mutableStateOf<List<SecondaryFab>?>(null)
var autoCancel by mutableStateOf(true)
private var hidden by mutableStateOf(false)
var expanded by mutableStateOf(false)
private val listeners = mutableListOf<OnExpandStateListener>()
private var mHidden by mutableStateOf(hidden)
private var mExpanded by mutableStateOf(expanded)
private var showPrimaryFab by mutableStateOf(true)
fun addOnExpandStateListener(listener: OnExpandStateListener) {
listeners.add(listener)
}
fun show() {
hidden = false
}
fun hide() {
expanded = false
hidden = true
}
fun hidePrimaryFab() {
mHidden = true
showPrimaryFab = false
hide()
}
@Composable
override fun Content() {
Mdc3Theme {
if (expanded) {
DisposableEffect(Unit) {
listeners.forEach { it.invoke(true) }
onDispose {
listeners.forEach { it.invoke(false) }
}
}
if (autoCancel) {
Spacer(
modifier = Modifier.fillMaxSize().pointerInput(expanded) {
detectTapGestures {
expanded = false
}
},
)
}
}
val coroutineScope = rememberCoroutineScope()
val appearState by animateFloatAsState(
targetValue = if (hidden) 0f else 1f,
animationSpec = tween(FAB_ANIMATE_TIME, if (mExpanded) FAB_ANIMATE_TIME else 0),
label = "hiddenState",
) { mHidden = hidden }
Box(
modifier = Modifier.fillMaxSize().padding(16.dp),
contentAlignment = Alignment.BottomEnd,
) {
Box(
modifier = Modifier.rotate(lerp(-90f, 0f, appearState)).scale(appearState),
contentAlignment = Alignment.Center,
) {
val animatedProgress by animateFloatMergePredictiveBackAsState(
enable = expanded,
animationSpec = tween(FAB_ANIMATE_TIME, if (mHidden) FAB_ANIMATE_TIME else 0),
finishedListener = { mExpanded = expanded },
) { expanded = false }
FloatingActionButton(
onClick = { expanded = !expanded },
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = if (showPrimaryFab) {
Modifier.rotate(lerp(0f, -135f, animatedProgress))
} else {
Modifier
},
)
}
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())
}
}.alpha(1 - animatedProgress),
) {
Icon(imageVector = imageVector, contentDescription = null)
}
}
}
}
}
}
}
}
const val FAB_ANIMATE_TIME = 300
@@ -1,336 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.animation.Animator
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import com.google.android.material.math.MathUtils.dist
import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.SimpleHandler
import com.hippo.ehviewer.util.dp2px
import kotlin.math.abs
class FastScroller : View {
private var mDraggable = false
private var mMinHandlerHeight = 0
private var mRecyclerView: RecyclerView? = null
private var mOnScrollChangeListener: RecyclerView.OnScrollListener? = null
private var mAdapter: RecyclerView.Adapter<*>? = null
private var mAdapterDataObserver: AdapterDataObserver? = null
private var mHandler: Drawable? = null
private var mHandlerOffset = INVALID
private var mHandlerHeight = INVALID
private var mDownX = INVALID.toFloat()
private var mDownY = INVALID.toFloat()
private var mLastMotionY = INVALID.toFloat()
private var mDragged = false
private var mCantDrag = false
private var mTouchSlop = 0
private var mListener: OnDragHandlerListener? = null
private var mShowAnimator: ObjectAnimator? = null
private var mHideAnimator: ObjectAnimator? = null
private val mHideRunnable = Runnable { mHideAnimator!!.start() }
constructor(context: Context) : super(context) {
init(context, null, 0)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs, 0)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr,
) {
init(context, attrs, defStyleAttr)
}
private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
val a = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr, 0)
mHandler = a.getDrawable(R.styleable.FastScroller_handler)
mDraggable = a.getBoolean(R.styleable.FastScroller_draggable, true)
a.recycle()
alpha = 0.0f
visibility = INVISIBLE
mMinHandlerHeight = dp2px(context, MIN_HANDLER_HEIGHT_DP.toFloat())
mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
mShowAnimator = ObjectAnimator.ofFloat(this, "alpha", 1.0f).apply {
interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR
duration = SCROLL_BAR_FADE_DURATION.toLong()
}
mHideAnimator = ObjectAnimator.ofFloat(this, "alpha", 0.0f).apply {
interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR
duration = SCROLL_BAR_FADE_DURATION.toLong()
addListener(object : SimpleAnimatorListener() {
private var mCancel = false
override fun onAnimationCancel(animation: Animator) {
mCancel = true
}
override fun onAnimationEnd(animation: Animator) {
if (mCancel) {
mCancel = false
} else {
visibility = INVISIBLE
}
}
})
}
}
fun setOnDragHandlerListener(listener: OnDragHandlerListener?) {
mListener = listener
}
private fun updatePosition(show: Boolean) {
if (mRecyclerView == null) {
return
}
val paddingTop = paddingTop
val paddingBottom = paddingBottom
val height = height - paddingTop - paddingBottom
val offset = mRecyclerView!!.computeVerticalScrollOffset()
val extent = mRecyclerView!!.computeVerticalScrollExtent()
val range = mRecyclerView!!.computeVerticalScrollRange()
if (height <= 0 || extent >= range || extent <= 0) {
return
}
var endOffset = height * offset / range
var endHeight = height * extent / range
endHeight = endHeight.coerceAtLeast(mMinHandlerHeight)
endOffset = endOffset.coerceAtMost(height - endHeight)
mHandlerOffset = endOffset + paddingTop
mHandlerHeight = endHeight
if (show) {
if (mHideAnimator!!.isRunning) {
mHideAnimator!!.cancel()
mShowAnimator!!.start()
} else if (visibility != VISIBLE && !mShowAnimator!!.isRunning) {
visibility = VISIBLE
mShowAnimator!!.start()
}
SimpleHandler.removeCallbacks(mHideRunnable)
if (!mDragged) {
SimpleHandler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())
}
}
}
fun setHandlerDrawable(drawable: Drawable?) {
mHandler = drawable
invalidate()
}
var isDraggable: Boolean
get() = mDraggable
set(draggable) {
mDraggable = draggable
if (mDragged) {
mDragged = false
}
SimpleHandler.removeCallbacks(mHideRunnable)
mHideRunnable.run()
}
val isAttached: Boolean
get() = mRecyclerView != null
fun attachToRecyclerView(recyclerView: RecyclerView?) {
if (recyclerView == null) {
return
}
check(mRecyclerView == null) {
"The FastScroller is already attached to a RecyclerView, " +
"call detachedFromRecyclerView first"
}
mRecyclerView = recyclerView
mOnScrollChangeListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
updatePosition(true)
invalidate()
}
}
recyclerView.addOnScrollListener(mOnScrollChangeListener!!)
mAdapter = recyclerView.adapter
if (mAdapter != null) {
mAdapterDataObserver = object : AdapterDataObserver() {
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
super.onItemRangeChanged(positionStart, itemCount, payload)
updatePosition(false)
invalidate()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
updatePosition(false)
invalidate()
}
}
mAdapter!!.registerAdapterDataObserver(mAdapterDataObserver!!)
}
}
fun detachedFromRecyclerView() {
if (mRecyclerView != null && mOnScrollChangeListener != null) {
mRecyclerView!!.removeOnScrollListener(mOnScrollChangeListener!!)
}
mRecyclerView = null
mOnScrollChangeListener = null
if (mAdapter != null && mAdapterDataObserver != null) {
mAdapter!!.unregisterAdapterDataObserver(mAdapterDataObserver!!)
}
mAdapter = null
mAdapterDataObserver = null
alpha = 0.0f
visibility = INVISIBLE
}
override fun onDraw(canvas: Canvas) {
if (mRecyclerView == null || mHandler == null) {
return
}
if (mHandlerHeight == INVALID) {
updatePosition(false)
}
if (mHandlerHeight == INVALID) {
return
}
val paddingLeft = paddingLeft
val saved = canvas.save()
canvas.translate(paddingLeft.toFloat(), mHandlerOffset.toFloat())
mHandler!!.setBounds(0, 0, width - paddingLeft - paddingRight, mHandlerHeight)
mHandler!!.draw(canvas)
canvas.restoreToCount(saved)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!mDraggable || visibility != VISIBLE || mRecyclerView == null || mHandlerHeight == INVALID) {
return false
}
val action = event.action
if (action == MotionEvent.ACTION_DOWN) {
mCantDrag = false
}
if (mCantDrag) {
return false
}
when (action) {
MotionEvent.ACTION_DOWN -> {
mDragged = false
mDownX = event.x
mDownY = event.y
if (mDownY < mHandlerOffset || mDownY > mHandlerOffset + mHandlerHeight) {
mCantDrag = true
return false
}
}
MotionEvent.ACTION_MOVE -> {
if (!mDragged) {
val x = event.x
val y = event.y
// Check touch slop
if (dist(x, y, mDownX, mDownY) < mTouchSlop) {
return true
}
if (abs(x - mDownX) > abs(y - mDownY) || y < mHandlerOffset || y > mHandlerOffset + mHandlerHeight) {
mCantDrag = true
return false
} else {
mDragged = true
SimpleHandler.removeCallbacks(mHideRunnable)
// Update mLastMotionY
mLastMotionY =
if (mDownY < mHandlerOffset || mDownY >= mHandlerOffset + mHandlerHeight) {
// the point out of handler, make the point in handler center
(mHandlerOffset + mHandlerHeight / 2).toFloat()
} else {
mDownY
}
// Notify
if (mListener != null) {
mListener!!.onStartDragHandler()
}
}
}
val range = mRecyclerView!!.computeVerticalScrollRange()
if (range <= 0) {
return true
}
val y = event.y
val scroll =
(range * (y - mLastMotionY) / (height - paddingTop - paddingBottom)).toInt()
mRecyclerView!!.scrollBy(0, scroll)
mLastMotionY = y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// Notify
if (mDragged && mListener != null) {
mListener!!.onEndDragHandler()
}
mDragged = false
SimpleHandler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())
}
}
return true
}
interface OnDragHandlerListener {
fun onStartDragHandler()
fun onEndDragHandler()
}
companion object {
private const val INVALID = -1
private const val SCROLL_BAR_FADE_DURATION = 500
private const val SCROLL_BAR_DELAY = 1000
private const val MIN_HANDLER_HEIGHT_DP = 48
}
}
@@ -1,82 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.RectF
import android.graphics.drawable.Drawable
class HandlerDrawable : Drawable() {
private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val mTemp = RectF()
private var mColor = Color.BLACK
init {
mPaint.color = mColor
mPaint.style = Paint.Style.FILL
}
override fun draw(canvas: Canvas) {
val width = bounds.width()
val height = bounds.height()
if (width > height) {
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), mPaint)
} else {
mTemp[0f, 0f, width.toFloat()] = width.toFloat()
canvas.drawArc(mTemp, -180f, 180f, true, mPaint)
mTemp[0f, (height - width).toFloat(), width.toFloat()] = height.toFloat()
canvas.drawArc(mTemp, 0f, 180f, true, mPaint)
val halfWidth = width.toFloat() / 2.0f
canvas.drawRect(0f, halfWidth, width.toFloat(), height - halfWidth, mPaint)
}
}
fun setColor(color: Int) {
if (mColor != color) {
mColor = color
mPaint.color = color
}
}
override fun setAlpha(alpha: Int) {
mPaint.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.colorFilter = colorFilter
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
return when (Color.alpha(mColor)) {
0xff -> {
PixelFormat.OPAQUE
}
0x00 -> {
PixelFormat.TRANSPARENT
}
else -> {
PixelFormat.TRANSLUCENT
}
}
}
}
@@ -1,28 +0,0 @@
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)
}
override fun onInterceptTouchEvent(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.onInterceptTouchEvent(e)
}
}
@@ -1,109 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.view.ViewPropertyAnimator
open class ViewTransition(vararg views: View) {
private val mViews: Array<View>
protected var mAnimator1: ViewPropertyAnimator? = null
protected var mAnimator2: ViewPropertyAnimator? = null
var shownViewIndex = -1
private set
private var mOnShowViewListener: OnShowViewListener? = null
init {
check(views.size >= 2) { "You must pass view to ViewTransition" }
mViews = arrayOf(*views)
showView(0, false)
}
fun setOnShowViewListener(listener: OnShowViewListener?) {
mOnShowViewListener = listener
}
fun showView(shownView: Int, animation: Boolean = true): Boolean {
val views = mViews
val length = views.size
if (shownView >= length || shownView < 0) {
throw IndexOutOfBoundsException(
"Only " + length + " view(s) in " +
"the ViewTransition, but attempt to show " + shownView,
)
}
return if (shownViewIndex != shownView) {
val oldShownView = shownViewIndex
shownViewIndex = shownView
// Cancel animation
if (mAnimator1 != null) {
mAnimator1!!.cancel()
}
if (mAnimator2 != null) {
mAnimator2!!.cancel()
}
if (animation) {
startAnimations(views[oldShownView], views[shownView])
} else {
for (i in 0 until length) {
val v = views[i]
if (i == shownView) {
v.alpha = 1f
v.visibility = View.VISIBLE
} else {
v.alpha = 0f
v.visibility = View.GONE
}
}
}
if (null != mOnShowViewListener) {
mOnShowViewListener!!.onShowView(views[oldShownView], views[shownView])
}
true
} else {
false
}
}
protected open fun startAnimations(hiddenView: View, shownView: View) {
mAnimator1 = hiddenView.animate().alpha(0f)
mAnimator1!!.setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
hiddenView.visibility = View.GONE
mAnimator1 = null
}
}).start()
shownView.alpha = 0f
shownView.visibility = View.VISIBLE
mAnimator2 = shownView.animate().alpha(1f)
mAnimator2!!.setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mAnimator2 = null
}
}).start()
}
interface OnShowViewListener {
fun onShowView(hiddenView: View, shownView: View)
}
companion object {
const val ANIMATE_TIME = 300L
}
}
@@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Reorder
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -29,7 +29,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -56,6 +55,7 @@ import com.hippo.ehviewer.util.sendTo
import com.hippo.unifile.UniFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import sh.calvin.reorderable.ReorderableItemScope
@Composable
private fun AsyncThumb(
@@ -120,13 +120,12 @@ private fun AsyncThumb(
}
@Composable
fun DownloadCard(
fun ReorderableItemScope.DownloadCard(
onClick: () -> Unit,
onThumbClick: () -> Unit,
onLongClick: () -> Unit,
onStart: () -> Unit,
onStop: () -> Unit,
onDrag: () -> Unit,
info: DownloadInfo,
modifier: Modifier = Modifier,
) {
@@ -243,13 +242,13 @@ fun DownloadCard(
bottom.linkTo(parent.bottom)
},
) {
IconButton(onClick = { }) {
IconButton(
onClick = { },
modifier = Modifier.draggableHandle(),
) {
Icon(
imageVector = Icons.Default.DragHandle,
imageVector = Icons.Default.Reorder,
contentDescription = null,
modifier = Modifier.pointerInput(info.gid) {
awaitPointerEventScope { onDrag() }
},
)
}
if (downloadState == DownloadInfo.STATE_WAIT || downloadState == DownloadInfo.STATE_DOWNLOAD) {
@@ -27,7 +27,6 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.hippo.ehviewer.ui.legacy.FAB_ANIMATE_TIME
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
import eu.kanade.tachiyomi.util.lang.launchIO
import moe.tarsin.coroutines.runSuspendCatching
@@ -115,3 +114,5 @@ fun FabLayout(
}
}
}
const val FAB_ANIMATE_TIME = 300
@@ -18,34 +18,12 @@ package com.hippo.ehviewer.ui.scene
import android.content.res.Resources.Theme
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.fragment.app.Fragment
import com.hippo.ehviewer.ui.MainActivity
import rikka.core.res.resolveDrawable
abstract class BaseScene : Fragment() {
val isDrawerLocked
get() = mainActivity?.drawerLocked == true
fun lockDrawer() {
mainActivity?.drawerLocked = true
}
fun unlockDrawer() {
mainActivity?.drawerLocked = false
}
fun openDrawer() = mainActivity?.openDrawer()
fun showTip(message: CharSequence?, length: Int) {
mainActivity?.showTip(message!!, length)
}
fun showTip(@StringRes id: Int, length: Int) {
mainActivity?.showTip(id, length)
}
open val enableDrawerGestures = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -53,16 +31,12 @@ abstract class BaseScene : Fragment() {
view.background = requireActivity().theme.resolveDrawable(android.R.attr.windowBackground)
// Update left drawer locked state
if (enableDrawerGestures) {
unlockDrawer()
} else {
lockDrawer()
}
mainActivity?.drawerLocked = !enableDrawerGestures
hideSoftInput()
mainActivity?.recompose()
}
val mainActivity: MainActivity?
private val mainActivity: MainActivity?
get() = activity as? MainActivity
private fun hideSoftInput() = activity?.window?.decorView?.run {
@@ -75,6 +49,5 @@ abstract class BaseScene : Fragment() {
companion object {
const val LENGTH_SHORT = 0
const val LENGTH_LONG = 1
const val KEY_DRAWER_VIEW_STATE = "com.hippo.ehviewer.ui.scene.BaseScene:DRAWER_VIEW_STATE"
}
}
@@ -0,0 +1,542 @@
package com.hippo.ehviewer.ui.scene
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text2.input.rememberTextFieldState
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.Add
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Reorder
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissValue
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.History
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.confirmRemoveDownloadRange
import com.hippo.ehviewer.ui.main.DownloadCard
import com.hippo.ehviewer.ui.main.FabLayout
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.showMoveDownloadLabelList
import com.hippo.ehviewer.ui.tools.Deferred
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
import com.hippo.ehviewer.util.containsIgnoreCase
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.mapToLongArray
import com.ramcosta.composedestinations.annotation.Destination
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import kotlin.math.roundToInt
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyColumnState
@Destination
@Composable
fun DownloadsScreen(navigator: NavController) {
var label by rememberSaveable { mutableStateOf<String?>(null) }
var keyword by rememberSaveable { mutableStateOf<String?>(null) }
var filterType by rememberSaveable { mutableStateOf(-1) }
var searchBarOffsetY by remember { mutableStateOf(0) }
val searchFieldState = rememberTextFieldState()
var selectMode by remember { mutableStateOf(false) }
val checkedInfoMap = remember { mutableStateMapOf<Long, DownloadInfo>() }
SideEffect {
if (checkedInfoMap.isEmpty()) selectMode = false
}
val context = LocalContext.current
val activity = remember(context) { context.findActivity<MainActivity>() }
val coroutineScope = rememberCoroutineScope()
val dialogState = LocalDialogState.current
val title = stringResource(R.string.scene_download_title, label ?: stringResource(R.string.download_all))
val hint = stringResource(R.string.search_bar_hint, title)
val defaultName = stringResource(R.string.default_download_label_name)
val allName = stringResource(R.string.download_all)
val list = remember(label) {
when (label) {
null -> DownloadManager.allInfoList
defaultName -> DownloadManager.defaultInfoList
else -> DownloadManager.getLabelDownloadInfoList(label) ?: DownloadManager.allInfoList.also { label = null }
}.filter { info ->
(filterType == -1 || info.state == filterType) && keyword?.let { info.title.containsIgnoreCase(it) || info.titleJpn.containsIgnoreCase(it) || info.uploader.containsIgnoreCase(it) } ?: true
}.map { it.copy(downloadInfo = it.downloadInfo.copy()) }
}
val labelsList = DownloadManager.labelList
val searchBarConnection = remember {
object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
searchBarOffsetY = (searchBarOffsetY + consumed.y).roundToInt().coerceIn(-300, 0)
return Offset.Zero // We never consume it
}
}
}
val newLabel = stringResource(R.string.new_label_title)
val labelsStr = stringResource(R.string.download_labels)
val labelEmpty = stringResource(R.string.label_text_is_empty)
val defaultInvalid = stringResource(R.string.label_text_is_invalid)
val labelExists = stringResource(R.string.label_text_exist)
with(activity) {
ProvideSideSheetContent { drawerState ->
fun closeSheet() = coroutineScope.launch { drawerState.close() }
TopAppBar(
title = { Text(text = labelsStr) },
windowInsets = WindowInsets(0, 0, 0, 0),
actions = {
IconButton(
onClick = {
coroutineScope.launch {
val text = dialogState.awaitInputText(title = newLabel, hint = labelsStr) { text ->
when {
text.isBlank() -> labelEmpty
text == defaultName -> defaultInvalid
DownloadManager.containLabel(text) -> labelExists
else -> null
}
}
DownloadManager.addLabel(text)
}
},
) {
Icon(imageVector = Icons.Default.Add, contentDescription = null)
}
val letMeSelect = stringResource(R.string.let_me_select)
IconButton(
onClick = {
coroutineScope.launch {
dialogState.showSelectActions(R.string.default_download_label) {
onSelect(letMeSelect) {
Settings.hasDefaultDownloadLabel = false
}
onSelect(defaultName) {
Settings.hasDefaultDownloadLabel = true
Settings.defaultDownloadLabel = defaultName
}
DownloadManager.labelList.forEach { (label) ->
onSelect(label) {
Settings.hasDefaultDownloadLabel = true
Settings.defaultDownloadLabel = label
}
}
}
}
},
) {
Icon(imageVector = Icons.Default.Archive, contentDescription = null)
}
},
)
val labelsListState = rememberLazyListState()
val reorderableLabelState = rememberReorderableLazyColumnState(labelsListState) { from, to ->
coroutineScope.launch {
DownloadManager.moveLabel(from.index - 2, to.index - 2)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = labelsListState,
contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
) {
stickyHeader {
val all = DownloadManager.allInfoList
ListItem(
modifier = Modifier.clickable {
label = null
closeSheet()
},
tonalElevation = 1.dp,
shadowElevation = 1.dp,
headlineContent = {
Text("$allName [${all.size}]")
},
)
}
stickyHeader {
val default = DownloadManager.defaultInfoList
ListItem(
modifier = Modifier.clickable {
label = defaultName
closeSheet()
},
tonalElevation = 1.dp,
shadowElevation = 1.dp,
headlineContent = {
Text("$defaultName [${default.size}]")
},
)
}
items(labelsList, key = { it.label }) { (item) ->
val dismissState = rememberDismissState(
confirmValueChange = {
if (it == DismissValue.DismissedToStart) {
coroutineScope.launch {
DownloadManager.deleteLabel(item)
}
}
true
},
)
ReorderableItem(reorderableLabelState, key = item) { isDragging ->
LocalTouchSlopProvider(Settings.touchSlopFactor.toFloat()) {
SwipeToDismissBox(
state = dismissState,
backgroundContent = {},
directions = setOf(DismissDirection.EndToStart),
) {
val elevation by animateDpAsState(
if (isDragging) {
8.dp // md.sys.elevation.level4
} else {
1.dp // md.sys.elevation.level1
},
label = "elevation",
)
val thatList = DownloadManager.getLabelDownloadInfoList(item)
val text = if (thatList != null) "$item [${thatList.size}]" else item
ListItem(
modifier = Modifier.clickable {
label = item
closeSheet()
},
tonalElevation = 1.dp,
shadowElevation = elevation,
headlineContent = {
Text(text)
},
trailingContent = {
Row {
IconButton(
onClick = {
coroutineScope.launch {
val new = dialogState.awaitInputText(title = newLabel, hint = labelsStr) { text ->
when {
text.isBlank() -> labelEmpty
text == defaultName -> defaultInvalid
DownloadManager.containLabel(text) -> labelExists
else -> null
}
}
DownloadManager.renameLabel(item, new)
}
},
) {
Icon(imageVector = Icons.Default.Edit, contentDescription = null)
}
IconButton(
onClick = {},
modifier = Modifier.draggableHandle(),
) {
Icon(imageVector = Icons.Default.Reorder, contentDescription = null)
}
}
},
)
}
}
}
}
}
}
}
SearchBarScreen(
title = title,
searchFieldState = searchFieldState,
searchFieldHint = hint,
showSearchFab = selectMode,
onApplySearch = { keyword = it.takeUnless { it.isBlank() } },
onSearchHidden = { },
onSearchExpanded = { selectMode = false },
searchBarOffsetY = searchBarOffsetY,
trailingIcon = {
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { activity.openSideSheet() }) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = stringResource(id = R.string.download_labels))
}
IconButton(onClick = { expanded = !expanded }) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = null)
}
val states = stringArrayResource(id = R.array.download_state)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_filter)) },
onClick = {
expanded = false
coroutineScope.launch {
filterType = dialogState.showSingleChoice(states, filterType)
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_start_all)) },
onClick = {
expanded = false
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START_ALL
ContextCompat.startForegroundService(activity, intent)
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_stop_all)) },
onClick = {
expanded = false
coroutineScope.launchIO {
DownloadManager.stopAllDownload()
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_reset_reading_progress)) },
onClick = {
expanded = false
coroutineScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
) {
Text(text = stringResource(id = R.string.reset_reading_progress_message))
}
withNonCancellableContext {
DownloadManager.resetAllReadingProgress()
}
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_start_all_reversed)) },
onClick = {
expanded = false
val gidList = list.filter { it.state != DownloadInfo.STATE_FINISH }.asReversed().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(activity, intent)
},
)
}
},
) { contentPadding ->
val height by collectListThumbSizeAsState()
val layoutDirection = LocalLayoutDirection.current
val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h)
val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v)
val realPadding = PaddingValues(
top = contentPadding.calculateTopPadding() + marginV,
bottom = contentPadding.calculateBottomPadding() + marginV,
start = contentPadding.calculateStartPadding(layoutDirection) + marginH,
end = contentPadding.calculateEndPadding(layoutDirection) + marginH,
)
val listState = rememberLazyListState()
val reorderableState = rememberReorderableLazyColumnState(listState) { from, to ->
val fromIndex = from.index - 1
val toIndex = to.index - 1
val fromItem = list[fromIndex]
val toItem = list[toIndex]
val newList = DownloadManager.moveDownload(fromItem, toItem)
coroutineScope.launchIO {
EhDB.updateDownloadInfo(newList)
}
}
FastScrollLazyColumn(
modifier = Modifier.nestedScroll(searchBarConnection),
state = listState,
contentPadding = realPadding,
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)),
) {
items(list, key = { it.gid }) { info ->
ReorderableItem(reorderableState, key = info.gid) {
val checked = info.gid in checkedInfoMap
CheckableItem(checked = checked) {
DownloadCard(
onClick = {
if (selectMode) {
if (checked) {
checkedInfoMap.remove(info.gid)
} else {
checkedInfoMap[info.gid] = info
}
} else {
context.navToReader(info.galleryInfo)
}
},
onThumbClick = {
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailFragment.KEY_ARGS to GalleryInfoArgs(info.galleryInfo)),
)
},
onLongClick = {
checkedInfoMap[info.gid] = info
selectMode = true
},
onStart = {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo)
ContextCompat.startForegroundService(activity, intent)
},
onStop = {
coroutineScope.launchIO {
DownloadManager.stopDownload(info.gid)
}
},
info = info,
modifier = Modifier.height(height),
)
}
}
}
}
Deferred({ delay(200) }) {
if (list.isEmpty()) {
Column(
modifier = Modifier.padding(realPadding).fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
imageVector = EhIcons.Big.Default.History,
contentDescription = null,
modifier = Modifier.padding(16.dp),
)
Text(
text = stringResource(id = R.string.no_download_info),
style = MaterialTheme.typography.headlineMedium,
)
}
}
}
}
FabLayout(
hidden = !selectMode,
expanded = selectMode,
onExpandChanged = {
checkedInfoMap.clear()
selectMode = false
},
autoCancel = false,
) {
onClick(Icons.Default.DoneAll) {
val info = list.associateBy { it.gid }
checkedInfoMap.putAll(info)
throw CancellationException()
}
onClick(Icons.Default.PlayArrow) {
val gidList = checkedInfoMap.run { values.also { clear() } }.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)
}
onClick(Icons.Default.Pause) {
val gidList = checkedInfoMap.run { values.also { clear() } }.mapToLongArray(DownloadInfo::gid)
DownloadManager.stopRangeDownload(gidList)
}
onClick(Icons.Default.Delete) {
val infoList = checkedInfoMap.run { values.also { clear() } }
dialogState.confirmRemoveDownloadRange(infoList)
}
onClick(Icons.AutoMirrored.Default.DriveFileMove) {
val infoList = checkedInfoMap.run { values.also { clear() } }
dialogState.showMoveDownloadLabelList(infoList)
}
}
}
class DownloadsFragment : BaseScene() {
override val enableDrawerGestures = true
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val navController = findNavController()
return ComposeWithMD3 {
DownloadsScreen(navController)
}
}
companion object {
const val KEY_GID = "gid"
const val KEY_ACTION = "action"
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
}
}
@@ -1,823 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.height
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.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.viewbinding.ViewBinding
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.detailSize
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.databinding.DrawerListRvBinding
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
import com.hippo.ehviewer.databinding.SceneDownloadBinding
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadService.Companion.clear
import com.hippo.ehviewer.ui.confirmRemoveDownload
import com.hippo.ehviewer.ui.confirmRemoveDownloadRange
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.legacy.HandlerDrawable
import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.main.DownloadCard
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.containsIgnoreCase
import com.hippo.ehviewer.util.mapToLongArray
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
@SuppressLint("RtlHardcoded")
class DownloadsScene : SearchBarScene() {
/*---------------
Whole life cycle
---------------*/
private var mKeyword: String? = null
private var mLabel: String? = null
private val mList get() = mAdapter?.currentList
/*---------------
View life cycle
---------------*/
private var _binding: SceneDownloadBinding? = null
private val binding get() = _binding!!
private var mViewTransition: ViewTransition? = null
private var mAdapter: DownloadAdapter? = null
private var mItemTouchHelper: ItemTouchHelper? = null
private val tracker get() = mAdapter!!.tracker!!
private var mGid = -1L
private var mLabelAdapter: DownloadLabelAdapter? = null
private lateinit var mLabels: MutableList<String>
private var mType = -1
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
override val fastScroller get() = binding.fastScroller
override val recyclerView get() = binding.recyclerView
private fun initLabels() {
context ?: return
val listLabel = DownloadManager.labelList
// Add "All" and "Default" label names
mLabels = arrayListOf(
getString(R.string.download_all),
getString(R.string.default_download_label_name),
)
listLabel.forEach {
mLabels.add(it.label)
}
}
private fun handleArguments(args: Bundle?): Boolean {
if (null == args) {
return false
}
if (ACTION_CLEAR_DOWNLOAD_SERVICE == args.getString(KEY_ACTION)) {
clear()
}
val gid = args.getLong(KEY_GID, -1L)
if (-1L != gid) {
val info = DownloadManager.getDownloadInfo(gid)
if (null != info) {
mLabel = info.label
mGid = gid
return true
}
}
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun filter(info: DownloadInfo) = info.run {
(mType == -1 || state == mType) &&
mKeyword?.let {
title.containsIgnoreCase(it) || titleJpn.containsIgnoreCase(it) ||
uploader.containsIgnoreCase(it)
} ?: true
}
private fun updateInfoList() {
mAdapter?.run {
val list = when (mLabel) {
null -> DownloadManager.allInfoList
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
?: DownloadManager.allInfoList.also { mLabel = null }
}.mapNotNull { it.takeIf(::filter)?.copy(downloadInfo = it.downloadInfo.copy()) }
submitList(list) {
// This may be called after view destroyed
_binding ?: return@submitList
if (mGid != -1L) {
val position = list.indexOfFirst { it.gid == mGid }
if (position != -1) {
binding.recyclerView.scrollToPosition(position)
}
mGid = -1L
}
updateView()
}
}
}
private fun updateForLabel() {
updateInfoList()
Settings.recentDownloadLabel = mLabel
}
private fun updateTitle() {
val title = getString(R.string.scene_download_title, mLabel ?: getString(R.string.download_all))
setTitle(title)
setSearchBarHint(getString(R.string.search_bar_hint, title))
}
private fun onInit() {
if (!handleArguments(arguments)) {
mLabel = Settings.recentDownloadLabel
}
}
private fun onRestore(savedInstanceState: Bundle) {
mLabel = savedInstanceState.getString(KEY_LABEL)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_LABEL, mLabel)
mAdapter?.let { tracker.saveSelection(outState) }
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup,
savedInstanceState: Bundle?,
): ViewBinding {
_binding = SceneDownloadBinding.inflate(inflater, container)
binding.run {
mViewTransition = ViewTransition(content, tip)
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_download)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
tip.setCompoundDrawables(null, drawable, null, null)
mAdapter = DownloadAdapter()
mAdapter!!.setHasStableIds(true)
recyclerView.adapter = mAdapter
mAdapter!!.tracker = GallerySelectionTracker(
"download-selection",
recyclerView,
{ mList!! },
{ (this as DownloadHolder).itemId },
).apply {
addCustomChoiceListener({
binding.fabLayout.show()
binding.fabLayout.expanded = true
lockDrawer()
}) {
binding.fabLayout.expanded = false
binding.fabLayout.hide()
unlockDrawer()
}
restoreSelection(savedInstanceState)
}
val layoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
layoutManager.setColumnSize(
resources.getDimensionPixelOffset(
when (detailSize.value) {
0 -> R.dimen.gallery_list_column_width_long
1 -> R.dimen.gallery_list_column_width_short
else -> throw IllegalStateException("Unexpected value: ${detailSize.value}")
},
),
)
recyclerView.layoutManager = layoutManager
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val decoration = object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.set(0, interval / 2, 0, interval / 2)
}
}
recyclerView.addItemDecoration(decoration)
mItemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled() = false
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return if (tracker.isInCustomChoice) {
0
} else {
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
if (fromPosition == toPosition) {
return false
}
val fromItem = mList!![fromPosition]
val toItem = mList!![toPosition]
val list = DownloadManager.moveDownload(fromItem, toItem)
updateInfoList()
lifecycleScope.launchIO {
EhDB.updateDownloadInfo(list)
}
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mList!![position]
lifecycleScope.launchUI {
dialogState.confirmRemoveDownload(info) {
mAdapter!!.notifyItemChanged(position)
}
}
}
})
mItemTouchHelper!!.attachToRecyclerView(recyclerView)
fastScroller.attachToRecyclerView(recyclerView)
val handlerDrawable = HandlerDrawable()
handlerDrawable.setColor(theme.resolveColor(com.google.android.material.R.attr.colorPrimary))
fastScroller.setHandlerDrawable(handlerDrawable)
with(fabLayout) {
autoCancel = false
hidePrimaryFab()
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()
dialogState.confirmRemoveDownloadRange(downloadInfoList)
},
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 {
DownloadManager.stateFlow().collectLatest {
updateInfoList()
}
}
setOnApplySearch {
mKeyword = it.takeUnless { it.isEmpty() }
updateInfoList()
}
return binding
}
override fun onSearchViewExpanded() {
binding.fabLayout.hide()
super.onSearchViewExpanded()
}
override fun onRelease() {
super.onRelease()
binding.recyclerView.stopScroll()
mViewTransition = null
mAdapter = null
mLabelAdapter = null
mItemTouchHelper = null
_binding = null
}
@Composable
override fun TrailingIcon() {
dialogState.Intercept()
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { openSideSheet() }) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = stringResource(id = R.string.download_labels))
}
IconButton(onClick = { expanded = !expanded }) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = null)
}
val states = stringArrayResource(id = R.array.download_state)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_filter)) },
onClick = {
expanded = false
lifecycleScope.launch {
val type = dialogState.showSingleChoice(states, mType)
if (type != mType) {
updateInfoList()
}
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_start_all)) },
onClick = {
expanded = false
val activity = requireActivity()
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START_ALL
ContextCompat.startForegroundService(activity, intent)
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_stop_all)) },
onClick = {
expanded = false
lifecycleScope.launchIO {
DownloadManager.stopAllDownload()
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_reset_reading_progress)) },
onClick = {
expanded = false
lifecycleScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
) {
Text(text = stringResource(id = R.string.reset_reading_progress_message))
}
withNonCancellableContext {
DownloadManager.resetAllReadingProgress()
}
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.download_start_all_reversed)) },
onClick = {
expanded = false
val list = mList ?: return@DropdownMenuItem
val activity = requireActivity()
val gidList = list.filter { it.state != DownloadInfo.STATE_FINISH }
.asReversed().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(activity, intent)
},
)
}
}
private fun updateView() {
if (mViewTransition != null) {
if (mList.isNullOrEmpty()) {
mViewTransition!!.showView(1)
} else {
mViewTransition!!.showView(0)
}
}
updateTitle()
}
override fun onCreateDrawerView(inflater: LayoutInflater): View {
val drawerBinding = DrawerListRvBinding.inflate(inflater)
drawerBinding.toolbar.setTitle(R.string.download_labels)
drawerBinding.toolbar.inflateMenu(R.menu.drawer_download)
drawerBinding.toolbar.setOnMenuItemClickListener { item: MenuItem ->
val id = item.itemId
if (id == R.id.action_add) {
val builder = EditTextDialogBuilder(
requireContext(),
null,
getString(R.string.download_labels),
)
builder.setTitle(R.string.new_label_title)
builder.setPositiveButton(android.R.string.ok, null)
val dialog = builder.show()
NewLabelDialogHelper(builder, dialog)
return@setOnMenuItemClickListener true
} else if (id == R.id.action_default_download_label) {
val list = DownloadManager.labelList.map { it.label }.toTypedArray()
val items = arrayOf(
getString(R.string.let_me_select),
getString(R.string.default_download_label_name),
*list,
)
BaseDialogBuilder(requireContext())
.setTitle(R.string.default_download_label)
.setItems(items) { _: DialogInterface?, which: Int ->
if (which == 0) {
Settings.hasDefaultDownloadLabel = false
} else {
Settings.hasDefaultDownloadLabel = true
val label: String? = if (which == 1) {
null
} else {
items[which]
}
Settings.defaultDownloadLabel = label
}
}.show()
return@setOnMenuItemClickListener true
}
false
}
initLabels()
mLabelAdapter = DownloadLabelAdapter(inflater)
drawerBinding.recyclerViewDrawer.layoutManager = LinearLayoutManager(context)
mLabelAdapter!!.setHasStableIds(true)
val itemTouchHelper = ItemTouchHelper(DownloadLabelItemTouchHelperCallback())
itemTouchHelper.attachToRecyclerView(drawerBinding.recyclerViewDrawer)
drawerBinding.recyclerViewDrawer.adapter = mLabelAdapter
return drawerBinding.root
}
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
RecyclerView.ViewHolder(binding.root)
private inner class DownloadLabelAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<DownloadLabelHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadLabelHolder {
val holder =
DownloadLabelHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
val label = mLabels[position].takeUnless { position == 0 }
if (mLabel != label) {
mKeyword = null
setSearchBarText(null)
mLabel = label
updateForLabel()
}
closeSideSheet()
}
holder.binding.edit.setOnClickListener {
val context = context
val label = mLabels[holder.bindingAdapterPosition]
if (context != null) {
val builder = EditTextDialogBuilder(
context,
label,
getString(R.string.download_labels),
)
builder.setTitle(R.string.rename_label_title)
builder.setPositiveButton(android.R.string.ok, null)
val dialog = builder.show()
RenameLabelDialogHelper(builder, dialog, label)
}
}
return holder
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: DownloadLabelHolder, position: Int) {
val label = mLabels[position]
val list: List<DownloadInfo?>? = when (position) {
0 -> {
DownloadManager.allInfoList
}
1 -> {
DownloadManager.defaultInfoList
}
else -> {
DownloadManager.getLabelDownloadInfoList(label)
}
}
holder.binding.run {
if (list != null) {
text.text = "$label [${list.size}]"
} else {
text.text = label
}
if (position < LABEL_OFFSET) {
edit.visibility = View.GONE
option.visibility = View.GONE
} else {
edit.visibility = View.VISIBLE
option.visibility = View.VISIBLE
}
}
}
override fun getItemId(position: Int): Long {
return (if (position < LABEL_OFFSET) position else mLabels[position].hashCode()).toLong()
}
override fun getItemCount(): Int {
return mLabels.size
}
}
private inner class MoveDialogHelper(
private val mLabels: Array<String>,
private val mDownloadInfoList: List<DownloadInfo>,
) : DialogInterface.OnClickListener {
@SuppressLint("NotifyDataSetChanged")
override fun onClick(dialog: DialogInterface, which: Int) {
context ?: return
val label: String? = if (which == 0) {
null
} else {
mLabels[which]
}
lifecycleScope.launchIO {
DownloadManager.changeLabel(mDownloadInfoList, label)
withUIContext {
if (mLabelAdapter != null) {
mLabelAdapter!!.notifyDataSetChanged()
}
}
}
}
}
class DownloadHolder(val view: ComposeView) : RecyclerView.ViewHolder(view)
private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() {
override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
}
private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) {
var tracker: GallerySelectionTracker<DownloadInfo>? = null
override fun getItemId(position: Int) = getItem(position).gid
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DownloadHolder(ComposeView(parent.context))
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
val info = getItem(position)
val checked = tracker?.isSelected(info.gid) ?: false
holder.view.setMD3Content {
val context = LocalContext.current
val height by collectListThumbSizeAsState()
key(info.gid) {
CheckableItem(checked = checked) {
DownloadCard(
onClick = {
context.navToReader(info.galleryInfo)
},
onThumbClick = {
navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info.galleryInfo)),
)
},
onLongClick = {
},
onStart = {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo)
ContextCompat.startForegroundService(requireActivity(), intent)
},
onStop = {
lifecycleScope.launchIO {
DownloadManager.stopDownload(info.gid)
}
},
onDrag = { mItemTouchHelper?.startDrag(holder) },
info = info,
modifier = Modifier.height(height),
)
}
}
}
}
}
private inner class RenameLabelDialogHelper(
private val mBuilder: EditTextDialogBuilder,
private val mDialog: AlertDialog,
private val mOriginalLabel: String?,
) : View.OnClickListener {
init {
val button: Button = mDialog.getButton(DialogInterface.BUTTON_POSITIVE)
button.setOnClickListener(this)
}
@SuppressLint("NotifyDataSetChanged")
override fun onClick(v: View) {
context ?: return
val text = mBuilder.text
if (text.isEmpty()) {
mBuilder.setError(getString(R.string.label_text_is_empty))
} else if (getString(R.string.default_download_label_name) == text) {
mBuilder.setError(getString(R.string.label_text_is_invalid))
} else if (DownloadManager.containLabel(text)) {
mBuilder.setError(getString(R.string.label_text_exist))
} else {
mBuilder.setError(null)
mDialog.dismiss()
lifecycleScope.launchIO {
DownloadManager.renameLabel(mOriginalLabel!!, text)
if (mLabelAdapter != null) {
withUIContext {
initLabels()
mLabelAdapter!!.notifyDataSetChanged()
if (mLabel == mOriginalLabel) {
mLabel = text
updateForLabel()
}
}
}
}
}
}
}
private inner class NewLabelDialogHelper(
private val mBuilder: EditTextDialogBuilder,
private val mDialog: AlertDialog,
) : View.OnClickListener {
init {
val button: Button = mDialog.getButton(DialogInterface.BUTTON_POSITIVE)
button.setOnClickListener(this)
}
override fun onClick(v: View) {
context ?: return
val text = mBuilder.text
if (text.isEmpty()) {
mBuilder.setError(getString(R.string.label_text_is_empty))
} else if (getString(R.string.default_download_label_name) == text) {
mBuilder.setError(getString(R.string.label_text_is_invalid))
} else if (DownloadManager.containLabel(text)) {
mBuilder.setError(getString(R.string.label_text_exist))
} else {
mBuilder.setError(null)
mDialog.dismiss()
lifecycleScope.launchIO {
DownloadManager.addLabel(text)
initLabels()
withUIContext {
mLabelAdapter?.notifyItemInserted(mLabels.size)
}
}
}
}
}
private inner class DownloadLabelItemTouchHelperCallback : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
): Int {
val position = viewHolder.bindingAdapterPosition
return if (position < LABEL_OFFSET) {
makeMovementFlags(0, 0)
} else {
makeMovementFlags(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT,
)
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
val context = context
if (null == context || fromPosition == toPosition || toPosition < LABEL_OFFSET) {
return false
}
lifecycleScope.launchIO {
DownloadManager.moveLabel(fromPosition - LABEL_OFFSET, toPosition - LABEL_OFFSET)
}
val item = mLabels.removeAt(fromPosition)
mLabels.add(toPosition, item)
mLabelAdapter!!.notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val label = mLabels[position]
BaseDialogBuilder(context!!)
.setMessage(getString(R.string.delete_label, label))
.setPositiveButton(R.string.delete) { _, _ ->
lifecycleScope.launchIO {
DownloadManager.deleteLabel(label)
mLabels.removeAt(position)
withUIContext {
mLabel = null
updateForLabel()
mLabelAdapter!!.notifyItemRemoved(position)
}
}
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}.setOnCancelListener {
mLabelAdapter!!.notifyItemChanged(position)
}.show()
}
}
companion object {
const val KEY_GID = "gid"
const val KEY_ACTION = "action"
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private const val KEY_LABEL = "label"
private const val LABEL_OFFSET = 2
private const val PAYLOAD_DOWNLOAD_INFO = 0
}
}
@@ -277,7 +277,7 @@ fun FavouritesScreen(navigator: NavController) {
} else {
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)),
bundleOf(GalleryDetailFragment.KEY_ARGS to GalleryInfoArgs(info)),
)
}
},
@@ -306,7 +306,7 @@ fun FavouritesScreen(navigator: NavController) {
} else {
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)),
bundleOf(GalleryDetailFragment.KEY_ARGS to GalleryInfoArgs(info)),
)
}
},
@@ -1,18 +1,3 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
@@ -1,18 +1,3 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.Manifest
@@ -550,7 +535,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: NavController)
) to {
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to TokenArgs(it.gid, it.token!!)),
bundleOf(GalleryDetailFragment.KEY_ARGS to TokenArgs(it.gid, it.token!!)),
)
}
}.toTypedArray()
@@ -1248,7 +1233,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: NavController)
}
}
class GalleryDetailScene : BaseScene() {
class GalleryDetailFragment : BaseScene() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val args = requireNotNull(arguments)
val composeArgs = requireNotNull(args.getParcelableCompat<GalleryDetailScreenArgs>(KEY_ARGS))
@@ -140,8 +140,8 @@ import com.hippo.ehviewer.image.Image.Companion.decodeBitmap
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.doGalleryInfoAction
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.FAB_ANIMATE_TIME
import com.hippo.ehviewer.ui.main.AdvancedSearchOption
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
import com.hippo.ehviewer.ui.main.FabLayout
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
@@ -573,7 +573,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) {
token: String,
) : UrlSuggestion() {
override val destination = R.id.galleryDetailScene
override val args = bundleOf(GalleryDetailScene.KEY_ARGS to TokenArgs(gid, token))
override val args = bundleOf(GalleryDetailFragment.KEY_ARGS to TokenArgs(gid, token))
}
class GalleryPageUrlSuggestion(
@@ -804,7 +804,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) {
onClick = {
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)),
bundleOf(GalleryDetailFragment.KEY_ARGS to GalleryInfoArgs(info)),
)
},
onLongClick = {
@@ -824,7 +824,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) {
onClick = {
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)),
bundleOf(GalleryDetailFragment.KEY_ARGS to GalleryInfoArgs(info)),
)
},
onLongClick = {
@@ -1,80 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.MotionEvent
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.selection.StorageStrategy
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import com.hippo.ehviewer.client.data.GalleryInfo
class GallerySelectionTracker<T : GalleryInfo>(
selectionId: String,
recyclerView: RecyclerView,
private val getItems: () -> List<T>,
getItemKey: RecyclerView.ViewHolder.() -> Long?,
builder: SelectionTracker.Builder<Long>.() -> Unit = {},
) {
private val adapter = recyclerView.adapter as? ConcatAdapter
private val tracker = SelectionTracker.Builder(
selectionId,
recyclerView,
object : ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int) = getItems()[position].gid
override fun getPosition(key: Long) =
if (adapter != null) {
adapter.adapters[0].itemCount
} else {
0
} + getItems().indexOfFirst { it.gid == key }
},
object : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent) =
recyclerView.findChildViewUnder(e.x, e.y)?.let {
recyclerView.getChildViewHolder(it).run {
object : ItemDetails<Long>() {
override fun getPosition() = absoluteAdapterPosition
override fun getSelectionKey() = getItemKey()
}
}
}
},
StorageStrategy.createLongStorage(),
).apply(builder).build()
var isInCustomChoice = false
private set
fun addCustomChoiceListener(onIntoCustomChoiceListener: () -> Unit, onOutOfCustomChoiceListener: () -> Unit) {
tracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
if (tracker.selection.isEmpty) {
if (isInCustomChoice) {
isInCustomChoice = false
onOutOfCustomChoiceListener()
}
} else if (!isInCustomChoice) {
isInCustomChoice = true
onIntoCustomChoiceListener()
}
}
override fun onSelectionRestored() {
if (!tracker.selection.isEmpty) {
isInCustomChoice = true
onIntoCustomChoiceListener()
}
}
})
}
fun isSelected(key: Long) = tracker.isSelected(key)
fun selectAll() = tracker.setItemsSelected(getItems().map(GalleryInfo::gid), true)
fun clearSelection() = tracker.clearSelection()
fun saveSelection(state: Bundle) = tracker.onSaveInstanceState(state)
fun restoreSelection(state: Bundle?) = tracker.onRestoreInstanceState(state)
fun getAndClearSelection() = getItems().run { tracker.selection.map { gid -> first { it.gid == gid } } }
.also { clearSelection() }
}
@@ -1,18 +1,3 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
@@ -153,7 +138,7 @@ fun HistoryScreen(navigator: NavController) {
onClick = {
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)),
bundleOf(GalleryDetailFragment.KEY_ARGS to GalleryInfoArgs(info)),
)
},
onLongClick = {
@@ -42,7 +42,7 @@ fun NavController.navWithUrl(url: String): Boolean {
GalleryDetailUrlParser.parse(url)?.apply {
navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to TokenArgs(gid, token)),
bundleOf(GalleryDetailFragment.KEY_ARGS to TokenArgs(gid, token)),
)
return true
}
@@ -53,7 +53,7 @@ fun ProgressScreen(gid: Long, token: String, page: Int, navigator: NavController
navigator.popBackStack()
navigator.navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to TokenArgs(gid, it, page)),
bundleOf(GalleryDetailFragment.KEY_ARGS to TokenArgs(gid, it, page)),
)
}
}.onFailure {
@@ -1,13 +1,6 @@
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
@@ -54,10 +47,8 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
@@ -68,26 +59,13 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
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
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import androidx.viewbinding.ViewBinding
import com.google.android.material.sidesheet.SideSheetDialog
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -95,10 +73,9 @@ 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.main.FAB_ANIMATE_TIME
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.getSparseParcelableArrayCompat
import com.jamal.composeprefs3.ui.ifNotNullThen
import com.jamal.composeprefs3.ui.ifTrueThen
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -382,163 +359,6 @@ fun SearchBarScreen(
}
}
abstract class SearchBarScene : BaseScene() {
private var drawerView: View? = null
private var drawerViewState: SparseArray<Parcelable>? = null
private var sideSheetDialog: SideSheetDialog? = null
private val searchFieldState = TextFieldState()
private var mSuggestionProvider: SuggestionProvider? = null
private var onApplySearch: (String) -> Unit = {}
private var searchFieldHint by mutableStateOf<String?>(null)
private var searchBarTitle by mutableStateOf<String?>(null)
var showSearchFab by mutableStateOf(false)
protected abstract val fabLayout: View
protected abstract val fastScroller: View
protected abstract val recyclerView: RecyclerView
override val enableDrawerGestures = true
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
var ofs by mutableIntStateOf(0)
val onScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
ofs = (ofs - dy).coerceIn(-300, 0)
}
}
return ComposeWithMD3 {
val compositionContext = rememberCompositionContext()
val density = LocalDensity.current
val margin = with(density) { 8.dp.roundToPx() }
SearchBarScreen(
title = searchBarTitle,
searchFieldState = searchFieldState,
searchFieldHint = searchFieldHint,
showSearchFab = showSearchFab,
onApplySearch = onApplySearch,
onSearchExpanded = ::onSearchViewExpanded,
onSearchHidden = ::onSearchViewHidden,
suggestionProvider = mSuggestionProvider,
searchBarOffsetY = ofs,
trailingIcon = {
TrailingIcon()
},
) { contentPadding ->
val topPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
val bottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
AndroidViewBinding(
modifier = Modifier.fillMaxSize(),
factory = { inflater, parent, _ ->
onCreateViewWithToolbar(inflater, parent, savedInstanceState).also {
recyclerView.addOnScrollListener(onScrollListener)
createDrawerView(savedInstanceState)?.apply {
if (this is ComposeView) {
val owner = viewLifecycleOwner
setViewTreeLifecycleOwner(owner)
setViewTreeViewModelStoreOwner(owner as ViewModelStoreOwner)
setViewTreeSavedStateRegistryOwner(owner as SavedStateRegistryOwner)
setParentCompositionContext(compositionContext)
}
sideSheetDialog = SideSheetDialog(parent.context).also {
it.setContentView(this)
}
}
}
},
onRelease = {
onRelease()
},
) {
fabLayout.updatePadding(bottom = bottomPadding)
fastScroller.updatePadding(top = topPadding + margin, bottom = bottomPadding)
recyclerView.updatePadding(top = topPadding + margin, bottom = bottomPadding)
}
}
}
}
abstract fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup,
savedInstanceState: Bundle?,
): ViewBinding
fun openSideSheet() = sideSheetDialog!!.show()
fun closeSideSheet() = sideSheetDialog!!.dismiss()
private fun createDrawerView(savedInstanceState: Bundle?): View? {
drawerView = onCreateDrawerView(layoutInflater)?.apply {
val saved = drawerViewState ?: savedInstanceState?.getSparseParcelableArrayCompat(KEY_DRAWER_VIEW_STATE)
saved?.let {
restoreHierarchyState(it)
}
}
return drawerView
}
open fun onCreateDrawerView(inflater: LayoutInflater): View? = null
private fun destroyDrawerView() {
drawerView?.let {
drawerViewState = SparseArray()
it.saveHierarchyState(drawerViewState)
}
onDestroyDrawerView()
drawerView = null
}
open fun onDestroyDrawerView() {
sideSheetDialog?.dismiss()
sideSheetDialog = null
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
drawerView?.let {
drawerViewState = SparseArray()
it.saveHierarchyState(drawerViewState)
outState.putSparseParcelableArray(KEY_DRAWER_VIEW_STATE, drawerViewState)
}
}
@CallSuper
open fun onRelease() {
destroyDrawerView()
}
@Composable
abstract fun TrailingIcon()
open fun onSearchViewExpanded() {}
open fun onSearchViewHidden() {}
fun setTitle(title: String?) {
searchBarTitle = title
}
fun setSearchBarText(text: String?) {
if (text != null) {
searchFieldState.setTextAndPlaceCursorAtEnd(text)
} else {
searchFieldState.clearText()
}
}
fun setSearchBarHint(hint: String?) {
searchFieldHint = hint
}
fun setOnApplySearch(lambda: (String) -> Unit) {
onApplySearch = lambda
}
fun setSuggestionProvider(suggestionProvider: SuggestionProvider) {
mSuggestionProvider = suggestionProvider
}
}
fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
return if (keyword.endsWith(':')) {
keyword
@@ -557,5 +377,3 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
}
}
}
const val SEARCH_VIEW_ANIMATE_TIME = 300L
@@ -323,9 +323,9 @@ class DialogState {
@StringRes title: Int? = null,
): R = showNoButton {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
title.ifNotNullThen {
if (title != null) {
Text(
text = stringResource(id = title!!),
text = stringResource(id = title),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
style = MaterialTheme.typography.headlineSmall,
)
@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
<com.hippo.ehviewer.ui.legacy.IgnoreCancelRV
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/gallery_list_margin_h"
android:paddingVertical="@dimen/gallery_list_margin_v" />
<com.hippo.ehviewer.ui.legacy.FastScroller
android:id="@+id/fast_scroller"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:paddingStart="20dp"
android:paddingEnd="4dp"
android:paddingBottom="8dp" />
</FrameLayout>
<TextView
android:id="@+id/tip"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="228dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="16dp"
android:gravity="center_horizontal"
android:text="@string/no_download_info" />
<com.hippo.ehviewer.ui.legacy.FabLayout
android:id="@+id/fab_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</merge>
+2 -2
View File
@@ -45,7 +45,7 @@
</fragment>
<fragment
android:id="@+id/galleryDetailScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryDetailScene"
android:name="com.hippo.ehviewer.ui.scene.GalleryDetailFragment"
android:label="GalleryDetailScene" />
<fragment
android:id="@+id/galleryCommentsScene"
@@ -65,7 +65,7 @@
android:label="HistoryScene" />
<fragment
android:id="@+id/nav_downloads"
android:name="com.hippo.ehviewer.ui.scene.DownloadsScene"
android:name="com.hippo.ehviewer.ui.scene.DownloadsFragment"
android:label="DownloadsScene" />
<activity
android:id="@+id/nav_settings"