diff --git a/app/src/main/kotlin/androidx/swiperefreshlayout/widget/ThemedSwipeRefreshLayout.kt b/app/src/main/kotlin/androidx/swiperefreshlayout/widget/ThemedSwipeRefreshLayout.kt deleted file mode 100644 index 58d16d66c..000000000 --- a/app/src/main/kotlin/androidx/swiperefreshlayout/widget/ThemedSwipeRefreshLayout.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt index 22bcedad5..1abf4bc5f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -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> // All labels without default label - val labelList = runAssertingNotMainThread { EhDB.getAllDownloadLabelList() } as MutableList + val labelList = mutableStateListOf().apply { + addAll(runAssertingNotMainThread { EhDB.getAllDownloadLabelList() }) + } // Store download info with default label val defaultInfoList: LinkedList @@ -595,7 +599,7 @@ object DownloadManager : OnSpiderListener { /** * @param label Not allow new label */ - suspend fun changeLabel(list: List, label: String?) { + suspend fun changeLabel(list: Collection, 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) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadService.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadService.kt index ceaa8c59f..5bb82917b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadService.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadService.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt index 0f7375eb6..fc7c1820c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt @@ -353,7 +353,7 @@ suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () - } } -suspend fun DialogState.confirmRemoveDownloadRange(list: List) { +suspend fun DialogState.confirmRemoveDownloadRange(list: Collection) { 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) { + 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) } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt index 3ab394fdc..3c61df5cf 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt @@ -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)), ) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/AutoStaggeredGridLayoutManager.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/AutoStaggeredGridLayoutManager.kt deleted file mode 100644 index d3076797d..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/AutoStaggeredGridLayoutManager.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FabLayout.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FabLayout.kt deleted file mode 100644 index af00b16b0..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FabLayout.kt +++ /dev/null @@ -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 Unit> - -class FabLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0, -) : AbstractComposeView(context, attrs, defStyle) { - var secondaryFab by mutableStateOf?>(null) - var autoCancel by mutableStateOf(true) - - private var hidden by mutableStateOf(false) - var expanded by mutableStateOf(false) - private val listeners = mutableListOf() - - 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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FastScroller.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FastScroller.kt deleted file mode 100644 index 6152b8016..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/FastScroller.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/HandlerDrawable.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/HandlerDrawable.kt deleted file mode 100644 index 8459348d2..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/HandlerDrawable.kt +++ /dev/null @@ -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 - } - } - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/IgnoreCancelRV.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/IgnoreCancelRV.kt deleted file mode 100644 index 683606245..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/IgnoreCancelRV.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/ViewTransition.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/ViewTransition.kt deleted file mode 100644 index 2477b354d..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/ViewTransition.kt +++ /dev/null @@ -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 - 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 - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt index d68ade2d1..b7a158d7f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt @@ -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) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt index c74730104..6b977b280 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt @@ -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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/BaseScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/BaseScene.kt index 79ee9f7ea..9bde55d23 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/BaseScene.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/BaseScene.kt @@ -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" } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsFragment.kt new file mode 100644 index 000000000..b5c549941 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsFragment.kt @@ -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(null) } + var keyword by rememberSaveable { mutableStateOf(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() } + SideEffect { + if (checkedInfoMap.isEmpty()) selectMode = false + } + + val context = LocalContext.current + val activity = remember(context) { context.findActivity() } + 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" + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsScene.kt deleted file mode 100644 index c1dc8ce1b..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/DownloadsScene.kt +++ /dev/null @@ -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 - 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 = 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() { - 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? = 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, - private val mDownloadInfoList: List, - ) : 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() { - 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(diffCallback) { - var tracker: GallerySelectionTracker? = 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 - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt index 83f1ebab9..40f1acc88 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt @@ -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)), ) } }, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt index 51f02bd7f..233fa2269 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryCommentsFragment.kt @@ -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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryDetailScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryDetailFragment.kt similarity index 98% rename from app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryDetailScene.kt rename to app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryDetailFragment.kt index fd95ca870..64a1aedcc 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryDetailScene.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryDetailFragment.kt @@ -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(KEY_ARGS)) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt index f6c230f86..7fc7eb098 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt @@ -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 = { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GallerySelectionTracker.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GallerySelectionTracker.kt deleted file mode 100644 index c1284afd8..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GallerySelectionTracker.kt +++ /dev/null @@ -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( - selectionId: String, - recyclerView: RecyclerView, - private val getItems: () -> List, - getItemKey: RecyclerView.ViewHolder.() -> Long?, - builder: SelectionTracker.Builder.() -> Unit = {}, -) { - private val adapter = recyclerView.adapter as? ConcatAdapter - private val tracker = SelectionTracker.Builder( - selectionId, - recyclerView, - object : ItemKeyProvider(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() { - override fun getItemDetails(e: MotionEvent) = - recyclerView.findChildViewUnder(e.x, e.y)?.let { - recyclerView.getChildViewHolder(it).run { - object : ItemDetails() { - 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() { - 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() } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/HistoryFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/HistoryFragment.kt index 9ca8cd564..1b574d76a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/HistoryFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/HistoryFragment.kt @@ -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 = { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/MainNav.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/MainNav.kt index 9e8bfe382..f5a0438a2 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/MainNav.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/MainNav.kt @@ -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 } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/ProgressFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/ProgressFragment.kt index e7a210811..6515fcb18 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/ProgressFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/ProgressFragment.kt @@ -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 { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt index 9369a34ae..47c43c464 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/SearchBarScene.kt @@ -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? = 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(null) - private var searchBarTitle by mutableStateOf(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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt index 7590e51e8..3d47e1895 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt @@ -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, ) diff --git a/app/src/main/res/layout/scene_download.xml b/app/src/main/res/layout/scene_download.xml deleted file mode 100644 index 6534c42d2..000000000 --- a/app/src/main/res/layout/scene_download.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 185d64f32..86289f7b7 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -45,7 +45,7 @@