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:
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
|
||||
|
||||
+2
-17
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user