diff --git a/app/src/main/java/io/legato/kazusa/model/CacheBook.kt b/app/src/main/java/io/legato/kazusa/model/CacheBook.kt index 4481c2ed3..91423ae66 100644 --- a/app/src/main/java/io/legato/kazusa/model/CacheBook.kt +++ b/app/src/main/java/io/legato/kazusa/model/CacheBook.kt @@ -71,13 +71,12 @@ object CacheBook { } } - fun start(context: Context, book: Book, start: Int, end: Int) { - if (!book.isLocal) { + fun start(context: Context, book: Book, selectedIndices: List) { + if (!book.isLocal && selectedIndices.isNotEmpty()) { context.startService { action = IntentAction.start putExtra("bookUrl", book.bookUrl) - putExtra("start", start) - putExtra("end", end) + putIntegerArrayListExtra("indices", ArrayList(selectedIndices)) } } } @@ -184,6 +183,10 @@ object CacheBook { postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) } + fun addDownload(index: Int) { + addDownload(index, index) + } + @Synchronized private fun onSuccess(chapter: BookChapter) { onDownloadSet.remove(chapter.index) diff --git a/app/src/main/java/io/legato/kazusa/service/CacheBookService.kt b/app/src/main/java/io/legato/kazusa/service/CacheBookService.kt index 6f495fb0f..64078e6bb 100644 --- a/app/src/main/java/io/legato/kazusa/service/CacheBookService.kt +++ b/app/src/main/java/io/legato/kazusa/service/CacheBookService.kt @@ -78,12 +78,19 @@ class CacheBookService : BaseService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.action?.let { action -> when (action) { - IntentAction.start -> addDownloadData( - intent.getStringExtra("bookUrl"), - intent.getIntExtra("start", 0), - intent.getIntExtra("end", 0) - ) - + IntentAction.start -> { + val bookUrl = intent.getStringExtra("bookUrl") ?: return@let + val indices = intent.getIntegerArrayListExtra("indices") + if (indices != null && indices.isNotEmpty()) { + addDownloadData(bookUrl, indices) + } else { + addDownloadData( + bookUrl, + intent.getIntExtra("start", 0), + intent.getIntExtra("end", 0) + ) + } + } IntentAction.remove -> { val bookUrl = intent.getStringExtra("bookUrl") CacheBook.cacheBookMap[bookUrl]?.stop() @@ -103,12 +110,16 @@ class CacheBookService : BaseService() { postEvent(EventBus.UP_DOWNLOAD, "") } - private fun addDownloadData(bookUrl: String?, start: Int, end: Int) { + private fun addDownloadData(bookUrl: String?, indices: List) { bookUrl ?: return + if (indices.isEmpty()) return + execute { val cacheBook = CacheBook.getOrCreate(bookUrl) ?: return@execute - val chapterCount = appDb.bookChapterDao.getChapterCount(bookUrl) + val book = cacheBook.book + val chapterCount = appDb.bookChapterDao.getChapterCount(bookUrl) + if (chapterCount == 0) { mutex.withLock { val name = book.name @@ -116,31 +127,39 @@ class CacheBookService : BaseService() { kotlin.runCatching { WebBook.getBookInfoAwait(cacheBook.bookSource, book) }.onFailure { - val msg = "《$name》目录为空且加载详情页失败\n${it.localizedMessage}" - AppLog.put(msg, it, true) + AppLog.put( + "《$name》目录为空且加载详情页失败\n${it.localizedMessage}", + it, + true + ) return@execute } } + WebBook.getChapterListAwait(cacheBook.bookSource, book).onFailure { if (book.totalChapterNum > 0) { book.totalChapterNum = 0 book.update() } - val msg = "《$name》目录为空且加载目录失败\n${it.localizedMessage}" - AppLog.put(msg, it, true) + AppLog.put( + "《$name》目录为空且加载目录失败\n${it.localizedMessage}", + it, + true + ) return@execute }.getOrNull()?.let { toc -> appDb.bookChapterDao.insert(*toc.toTypedArray()) } + book.update() } } - val end2 = if (end < 0) { - book.lastChapterIndex - } else { - min(end, book.lastChapterIndex) + + //添加每一章到下载队列 + indices.forEach { index -> + cacheBook.addDownload(index) } - cacheBook.addDownload(start, end2) + notificationContent = CacheBook.downloadSummary upCacheBookNotification() }.onFinally { @@ -150,6 +169,11 @@ class CacheBookService : BaseService() { } } + private fun addDownloadData(bookUrl: String?, start: Int, end: Int) { + addDownloadData(bookUrl, (start..end).toList()) + } + + private fun removeDownload(bookUrl: String?) { CacheBook.cacheBookMap[bookUrl]?.stop() postEvent(EventBus.UP_DOWNLOAD, "") diff --git a/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheActivity.kt b/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheActivity.kt index 99b48a844..4eecf7b40 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheActivity.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheActivity.kt @@ -173,12 +173,8 @@ class CacheActivity : VMBaseActivity() R.id.menu_download_after -> { if (!CacheBook.isRun) { adapter.getItems().forEach { book -> - CacheBook.start( - this@CacheActivity, - book, - book.durChapterIndex, - book.lastChapterIndex - ) + val indices = (book.durChapterIndex..book.lastChapterIndex).toList() + CacheBook.start(this@CacheActivity, book, indices) } } else { CacheBook.stop(this@CacheActivity) @@ -188,12 +184,8 @@ class CacheActivity : VMBaseActivity() R.id.menu_download_all -> { if (!CacheBook.isRun) { adapter.getItems().forEach { book -> - CacheBook.start( - this@CacheActivity, - book, - 0, - book.lastChapterIndex - ) + val indices = (0..book.lastChapterIndex).toList() + CacheBook.start(this@CacheActivity, book, indices) } } else { CacheBook.stop(this@CacheActivity) diff --git a/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheAdapter.kt b/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheAdapter.kt index d8f40a05d..4ec1c9d01 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheAdapter.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/cache/CacheAdapter.kt @@ -102,10 +102,12 @@ class CacheAdapter(context: Context, private val callBack: CallBack) : if (!it.isStop()) { CacheBook.remove(context, book.bookUrl) } else { - CacheBook.start(context, book, 0, book.lastChapterIndex) + val indices = (0..book.lastChapterIndex).toList() + CacheBook.start(context, book, indices) } } ?: let { - CacheBook.start(context, book, 0, book.lastChapterIndex) + val indices = (0..book.lastChapterIndex).toList() + CacheBook.start(context, book, indices) } } } diff --git a/app/src/main/java/io/legato/kazusa/ui/book/read/BaseReadBookActivity.kt b/app/src/main/java/io/legato/kazusa/ui/book/read/BaseReadBookActivity.kt index 8df6c6cca..1ba1e1cf5 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/read/BaseReadBookActivity.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/read/BaseReadBookActivity.kt @@ -276,7 +276,8 @@ abstract class BaseReadBookActivity : val end = editEnd.text!!.toString().let { if (it.isEmpty()) book.totalChapterNum else it.toInt() } - CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1) + val indices = (start - 1..end - 1).toList() + CacheBook.start(this@BaseReadBookActivity, book, indices) } } cancelButton() diff --git a/app/src/main/java/io/legato/kazusa/ui/book/searchContent/SearchContentActivity.kt b/app/src/main/java/io/legato/kazusa/ui/book/searchContent/SearchContentActivity.kt index 1072e558f..ced2765b2 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/searchContent/SearchContentActivity.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/searchContent/SearchContentActivity.kt @@ -1,5 +1,8 @@ package io.legato.kazusa.ui.book.searchContent +//import io.legado.app.lib.theme.bottomBackground +//import io.legado.app.lib.theme.getPrimaryTextColor +//import io.legado.app.lib.theme.primaryTextColor import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle @@ -23,12 +26,9 @@ import io.legato.kazusa.databinding.ActivitySearchContentBinding import io.legato.kazusa.help.IntentData import io.legato.kazusa.help.book.BookHelp import io.legato.kazusa.help.book.isLocal -//import io.legado.app.lib.theme.bottomBackground -//import io.legado.app.lib.theme.getPrimaryTextColor -//import io.legado.app.lib.theme.primaryTextColor import io.legato.kazusa.ui.widget.recycler.UpLinearLayoutManager import io.legato.kazusa.ui.widget.recycler.VerticalDivider -import io.legato.kazusa.utils.applyNavigationBarMargin +import io.legato.kazusa.utils.applyNavigationBarPadding import io.legato.kazusa.utils.hideSoftInput import io.legato.kazusa.utils.invisible import io.legato.kazusa.utils.observeEvent @@ -64,7 +64,7 @@ class SearchContentActivity : // val bbg = bottomBackground // val btc = getPrimaryTextColor(ColorUtils.isColorLight(bbg)) // binding.llSearchBaseInfo.setBackgroundColor(bbg) - binding.llSearchBaseInfo.applyNavigationBarMargin() + binding.llSearchBaseInfo.applyNavigationBarPadding() // binding.tvCurrentSearchInfo.setTextColor(btc) // binding.ivSearchContentTop.setColorFilter(btc) // binding.ivSearchContentBottom.setColorFilter(btc) diff --git a/app/src/main/java/io/legato/kazusa/ui/book/toc/BookmarkAdapter.kt b/app/src/main/java/io/legato/kazusa/ui/book/toc/BookmarkAdapter.kt index d322f7c4a..6f24f7792 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/toc/BookmarkAdapter.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/toc/BookmarkAdapter.kt @@ -7,6 +7,7 @@ import io.legato.kazusa.base.adapter.RecyclerAdapter import io.legato.kazusa.data.entities.Bookmark import io.legato.kazusa.databinding.ItemBookmarkBinding import io.legato.kazusa.utils.gone +import splitties.views.onClick import splitties.views.onLongClick class BookmarkAdapter(context: Context, val callback: Callback) : @@ -30,17 +31,16 @@ class BookmarkAdapter(context: Context, val callback: Callback) : } override fun registerListener(holder: ItemViewHolder, binding: ItemBookmarkBinding) { - binding.root.setOnClickListener { + binding.root.onLongClick { getItem(holder.layoutPosition)?.let { bookmark -> callback.onLongClick(bookmark, holder.layoutPosition) } } - binding.root.onLongClick { + binding.root.onClick { getItem(holder.layoutPosition)?.let { bookmark -> callback.onClick(bookmark) } } - } interface Callback { diff --git a/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListAdapter.kt b/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListAdapter.kt index 5c14bc999..0fc7a63c0 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListAdapter.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListAdapter.kt @@ -6,6 +6,7 @@ import android.os.Handler import android.os.Looper import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView import io.legato.kazusa.R import io.legato.kazusa.base.adapter.DiffRecyclerAdapter import io.legato.kazusa.base.adapter.ItemViewHolder @@ -16,7 +17,6 @@ import io.legato.kazusa.help.book.ContentProcessor import io.legato.kazusa.help.config.AppConfig import io.legato.kazusa.lib.theme.ThemeUtils import io.legato.kazusa.utils.gone -import io.legato.kazusa.utils.longToastOnUi import io.legato.kazusa.utils.themeColor import io.legato.kazusa.utils.visible import kotlinx.coroutines.CoroutineScope @@ -27,28 +27,25 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap -class ChapterListAdapter(context: Context, val callback: Callback) : - DiffRecyclerAdapter(context) { +class ChapterListAdapter( + context: Context, + val callback: Callback +) : DiffRecyclerAdapter(context) { val cacheFileNames = hashSetOf() private val displayTitleMap = ConcurrentHashMap() + private val selectedIndices = LinkedHashSet() private val handler = Handler(Looper.getMainLooper()) + private var upDisplayTileJob: Job? = null override val diffItemCallback: DiffUtil.ItemCallback get() = object : DiffUtil.ItemCallback() { - - override fun areItemsTheSame( - oldItem: BookChapter, - newItem: BookChapter - ): Boolean { + override fun areItemsTheSame(oldItem: BookChapter, newItem: BookChapter): Boolean { return oldItem.index == newItem.index } - override fun areContentsTheSame( - oldItem: BookChapter, - newItem: BookChapter - ): Boolean { + override fun areContentsTheSame(oldItem: BookChapter, newItem: BookChapter): Boolean { return oldItem.bookUrl == newItem.bookUrl && oldItem.url == newItem.url && oldItem.isVip == newItem.isVip @@ -58,11 +55,8 @@ class ChapterListAdapter(context: Context, val callback: Callback) : && oldItem.wordCount == newItem.wordCount && oldItem.isVolume == newItem.isVolume } - } - private var upDisplayTileJob: Job? = null - override fun onCurrentListChanged() { super.onCurrentListChanged() callback.onListChanged() @@ -80,7 +74,6 @@ class ChapterListAdapter(context: Context, val callback: Callback) : val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules() val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule() val items = getItems() - val indices = ((startIndex until items.size) + (startIndex downTo 0)) .filter { it in items.indices } @@ -100,33 +93,132 @@ class ChapterListAdapter(context: Context, val callback: Callback) : } } + fun isInSelectionMode(): Boolean = selectedIndices.isNotEmpty() - private fun getDisplayTitle(chapter: BookChapter): String { - return displayTitleMap[chapter.title] ?: chapter.title + fun toggleSelection(chapter: BookChapter, position: Int? = null) { + val pos = position ?: getItems().indexOfFirst { it.index == chapter.index } + if (pos == -1) return + + if (selectedIndices.contains(chapter.index)) { + selectedIndices.remove(chapter.index) + } else { + selectedIndices.add(chapter.index) + } + notifyItemChanged(pos, true) + callback.onSelectionModeChanged(isInSelectionMode()) } - override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding { - return ItemChapterListBinding.inflate(inflater, parent, false) + fun getSelectedChapters(): List = + getItems().filter { selectedIndices.contains(it.index) } + + fun clearSelection() { + if (selectedIndices.isNotEmpty()) { + val prevSelected = selectedIndices.toList() + selectedIndices.clear() + prevSelected.forEach { index -> + val pos = getItems().indexOfFirst { it.index == index } + if (pos != -1) notifyItemChanged(pos, true) + } + callback.onSelectionModeChanged(false) + } } + fun selectAll() { + val items = getItems() + val changedIndices = items.map { it.index }.filter { !selectedIndices.contains(it) } + selectedIndices.clear() + selectedIndices.addAll(items.map { it.index }) + changedIndices.forEach { index -> + val pos = items.indexOfFirst { it.index == index } + if (pos != -1) notifyItemChanged(pos, true) + } + callback.onSelectionModeChanged(true) + } + + fun invertSelection() { + val items = getItems() + val newSelected = items.map { it.index }.toSet() - selectedIndices + val changedIndices = (selectedIndices + newSelected) // 之前选中 + 新选中 = 改变的项 + selectedIndices.clear() + selectedIndices.addAll(newSelected) + changedIndices.forEach { index -> + val pos = items.indexOfFirst { it.index == index } + if (pos != -1) notifyItemChanged(pos, true) + } + callback.onSelectionModeChanged(selectedIndices.isNotEmpty()) + } + + fun selectFrom() { + val items = getItems() + val startPos = selectedIndices.lastOrNull()?.let { lastIndex -> + items.indexOfFirst { it.index == lastIndex } + } ?: return + + if (startPos !in items.indices) return + + val changedIndices = mutableListOf() + for (i in startPos until items.size) { + val chapterIndex = items[i].index + if (!selectedIndices.contains(chapterIndex)) { + selectedIndices.add(chapterIndex) + changedIndices.add(i) + } + } + + changedIndices.forEach { notifyItemChanged(it, true) } + callback.onSelectionModeChanged(isInSelectionMode()) + } + + private fun getDisplayTitle(chapter: BookChapter): String = + displayTitleMap[chapter.title] ?: chapter.title + + override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding = + ItemChapterListBinding.inflate(inflater, parent, false) + override fun convert( holder: ItemViewHolder, binding: ItemChapterListBinding, item: BookChapter, payloads: MutableList ) { - binding.run { - val isDur = callback.durChapterIndex() == item.index - val cached = callback.isLocalBook - || item.isVolume - || cacheFileNames.contains(item.getFileName()) - if (payloads.isEmpty()) { + val isDur = callback.durChapterIndex() == item.index + val cached = + callback.isLocalBook || item.isVolume || cacheFileNames.contains(item.getFileName()) + binding.run { + if (payloads.isEmpty()) { + tvChapterName.text = getDisplayTitle(item) + tvChapterItem.foreground = + ThemeUtils.resolveDrawable(context, android.R.attr.selectableItemBackground) + + if (!item.tag.isNullOrEmpty() && !item.isVolume) { + tvTag.text = item.tag + tvTag.visible() + } else tvTag.gone() + + if (AppConfig.tocCountWords && !item.wordCount.isNullOrEmpty() && !item.isVolume) { + tvWordCount.text = item.wordCount + tvWordCount.visible() + } else tvWordCount.gone() + + if (item.isVip && !item.isPay) ivLocked.visible() else ivLocked.gone() + + upHasCache(binding, cached) + + } else { + tvChapterName.text = getDisplayTitle(item) + upHasCache(binding, cached) + } + + if (selectedIndices.contains(item.index)) { + ivVolume.gone() + tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurfaceBright)) + } else { if (item.isVolume) { ivVolume.visible() - tvChapterName.text = getDisplayTitle(item) tvChapterName.textSize = 12f tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorTertiary)) + tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface)) } else { ivVolume.gone() if (isDur) { @@ -136,70 +228,39 @@ class ChapterListAdapter(context: Context, val callback: Callback) : tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorOnSurface)) tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface)) } - tvChapterName.text = getDisplayTitle(item) - tvChapterItem.foreground = - ThemeUtils.resolveDrawable(context, android.R.attr.selectableItemBackground) } - - //卷名不显示 - if (!item.tag.isNullOrEmpty() && !item.isVolume) { - //更新时间规则 - tvTag.text = item.tag - tvTag.visible() - } else { - tvTag.gone() - } - if (AppConfig.tocCountWords && !item.wordCount.isNullOrEmpty() && !item.isVolume) { - //章节字数 - tvWordCount.text = item.wordCount - tvWordCount.visible() - } else { - tvWordCount.gone() - } - - if (item.isVip && !item.isPay) { - ivLocked.visible() - } else { - ivLocked.gone() - } - - upHasCache(binding, cached) - } else { - tvChapterName.text = getDisplayTitle(item) - upHasCache(binding, cached) } } } + override fun registerListener(holder: ItemViewHolder, binding: ItemChapterListBinding) { holder.itemView.setOnClickListener { - getItem(holder.layoutPosition)?.let { - callback.openChapter(it) + val pos = holder.bindingAdapterPosition + if (pos == RecyclerView.NO_POSITION) return@setOnClickListener + val item = getItem(pos) ?: return@setOnClickListener + + if (isInSelectionMode()) { + toggleSelection(item, pos) + } else { + callback.openChapter(item) } } + holder.itemView.setOnLongClickListener { - getItem(holder.layoutPosition)?.let { item -> - context.longToastOnUi(getDisplayTitle(item)) - } + val pos = holder.bindingAdapterPosition + if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener true + val item = getItem(pos) ?: return@setOnLongClickListener true + + toggleSelection(item, pos) true } } - private fun upHasCache(binding: ItemChapterListBinding, cached: Boolean) = - binding.apply { - when { - cached -> { - ivChecked.setImageResource(R.drawable.ic_download_done) - ivChecked.visible() - } - - else -> { - ivChecked.setImageResource(R.drawable.ic_outline_cloud_24) - ivChecked.visible() - } - } - } - + private fun upHasCache(binding: ItemChapterListBinding, cached: Boolean) = binding.apply { + ivChecked.setImageResource(if (cached) R.drawable.ic_download_done else R.drawable.ic_outline_cloud_24) + ivChecked.visible() + } interface Callback { val scope: CoroutineScope @@ -208,6 +269,6 @@ class ChapterListAdapter(context: Context, val callback: Callback) : fun openChapter(bookChapter: BookChapter) fun durChapterIndex(): Int fun onListChanged() + fun onSelectionModeChanged(enabled: Boolean) } - } \ No newline at end of file diff --git a/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListFragment.kt b/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListFragment.kt index 28fcfe780..f03d51125 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListFragment.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListFragment.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.graphics.Canvas import android.graphics.Rect import android.os.Bundle +import android.transition.TransitionManager import android.view.View import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toDrawable @@ -14,6 +15,7 @@ import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.transition.platform.MaterialFadeThrough import io.legato.kazusa.R import io.legato.kazusa.base.VMBaseFragment import io.legato.kazusa.constant.EventBus @@ -26,14 +28,15 @@ import io.legato.kazusa.help.book.isLocal import io.legato.kazusa.help.book.simulatedTotalChapterNum import io.legato.kazusa.model.CacheBook import io.legato.kazusa.ui.widget.recycler.UpLinearLayoutManager -import io.legato.kazusa.ui.widget.recycler.VerticalDivider import io.legato.kazusa.utils.VibrationUtils import io.legato.kazusa.utils.applyNavigationBarPadding import io.legato.kazusa.utils.dpToPx +import io.legato.kazusa.utils.gone import io.legato.kazusa.utils.observeEvent import io.legato.kazusa.utils.themeColor import io.legato.kazusa.utils.toastOnUi import io.legato.kazusa.utils.viewbindingdelegate.viewBinding +import io.legato.kazusa.utils.visible import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -76,7 +79,7 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapt val book = book ?: return toastOnUi("开始下载: ${adapter.getItem(position)?.title}") - CacheBook.start(requireContext(), book, chapter.index, chapter.index) + CacheBook.start(requireContext(), book, listOf(chapter.index)) VibrationUtils.vibratePattern(requireContext(), longArrayOf(0, 50, 30, 50), -1) adapter.notifyItemChanged(position) } @@ -147,7 +150,7 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapt itemTouchHelper.attachToRecyclerView(binding.recyclerView) binding.recyclerView.layoutManager = mLayoutManager - binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { override fun getItemOffsets( outRect: Rect, @@ -177,6 +180,38 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapt btnLocate.setOnClickListener { mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) } + btnSelectAll.setOnClickListener { + adapter.selectAll() + toastOnUi("已全选 ${adapter.itemCount} 个章节") + } + btnInvertSelection.setOnClickListener { + adapter.invertSelection() + toastOnUi("已反选") + } + btnFromSelection.setOnClickListener { + adapter.selectFrom() + toastOnUi("已选择之后的章节") + } + + btnDownloadSelected.setOnClickListener { + val chapters = adapter.getSelectedChapters() + if (chapters.isNotEmpty()) { + val book = book ?: return@setOnClickListener + val indices = chapters.map { it.index } + CacheBook.start(requireContext(), book, indices) + toastOnUi("开始下载 ${chapters.size} 个章节") + adapter.clearSelection() + } else { + toastOnUi("未选择章节") + } + } + btnExit.setOnClickListener { + adapter.clearSelection() + } + btnBookmarkSelection.setOnClickListener { + addSelectedChaptersBookmarkDirectly() + toastOnUi("已添加书签") + } } @SuppressLint("SetTextI18n") @@ -185,7 +220,9 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapt upChapterList(null) durChapterIndex = book.durChapterIndex binding.tvCurrentChapterInfo.text = - "${book.durChapterTitle}(${book.durChapterIndex + 1}/${book.simulatedTotalChapterNum()})" + "${book.durChapterTitle}" + binding.tvCurrentChapterAll.text = + "${book.durChapterIndex + 1}/${book.simulatedTotalChapterNum()}" initCacheFileNames(book) } } @@ -199,6 +236,42 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapt } } + fun addSelectedChaptersBookmarkDirectly() { + val book = viewModel.bookData.value ?: return + val selectedChapters = adapter.getSelectedChapters() + if (selectedChapters.isEmpty()) return + + lifecycleScope.launch { + withContext(IO) { + selectedChapters.forEach { chapter -> + val page = chapter.title + val bookmark = book.createBookMark().apply { + chapterIndex = chapter.index + chapterPos = 0 + chapterName = page + // 不设置 content 或 bookText,或者只存标题 + } + appDb.bookmarkDao.insert(bookmark) + } + } + + // 可选:清空选择状态并提示 + adapter.clearSelection() + } + } + + + override fun onSelectionModeChanged(enabled: Boolean) { + TransitionManager.beginDelayedTransition(binding.coordinatorLayout, MaterialFadeThrough()) + if (enabled) { + binding.llBase.gone() + binding.floatingToolbarBottom.visible() + } else { + binding.llBase.visible() + binding.floatingToolbarBottom.gone() + } + } + override fun observeLiveBus() { observeEvent>(EventBus.SAVE_CONTENT) { (book, chapter) -> viewModel.bookData.value?.bookUrl?.let { bookUrl -> diff --git a/app/src/main/java/io/legato/kazusa/ui/book/toc/TocActivity.kt b/app/src/main/java/io/legato/kazusa/ui/book/toc/TocActivity.kt index ff014e49a..cf5946854 100644 --- a/app/src/main/java/io/legato/kazusa/ui/book/toc/TocActivity.kt +++ b/app/src/main/java/io/legato/kazusa/ui/book/toc/TocActivity.kt @@ -2,6 +2,7 @@ package io.legato.kazusa.ui.book.toc +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.Menu @@ -16,8 +17,11 @@ import io.legato.kazusa.R import io.legato.kazusa.base.VMBaseActivity import io.legato.kazusa.data.entities.Book import io.legato.kazusa.databinding.ActivityChapterListBinding +import io.legato.kazusa.databinding.DialogDownloadChoiceBinding import io.legato.kazusa.help.book.isLocalTxt import io.legato.kazusa.help.config.AppConfig +import io.legato.kazusa.lib.dialogs.alert +import io.legato.kazusa.model.CacheBook //import io.legado.app.lib.theme.accentColor //import io.legado.app.lib.theme.primaryTextColor import io.legato.kazusa.model.ReadBook @@ -143,6 +147,8 @@ class TocActivity : VMBaseActivity(), TxtTocRuleDialog(viewModel.bookData.value?.tocUrl) ) + R.id.menu_download -> showDownloadDialog() + R.id.menu_split_long_chapter -> { viewModel.bookData.value?.let { book -> item.isChecked = !item.isChecked @@ -204,6 +210,32 @@ class TocActivity : VMBaseActivity(), } } + @SuppressLint("SetTextI18n") + fun showDownloadDialog() { + ReadBook.book?.let { book -> + alert(titleResource = R.string.offline_cache) { + val alertBinding = DialogDownloadChoiceBinding.inflate(layoutInflater).apply { + editStart.setText((book.durChapterIndex + 1).toString()) + editEnd.setText(book.totalChapterNum.toString()) + } + customView { alertBinding.root } + okButton { + alertBinding.run { + val start = editStart.text!!.toString().let { + if (it.isEmpty()) 0 else it.toInt() + } + val end = editEnd.text!!.toString().let { + if (it.isEmpty()) book.totalChapterNum else it.toInt() + } + val indices = (start - 1..end - 1).toList() + CacheBook.start(this@TocActivity, book, indices) + } + } + cancelButton() + } + } + } + @Suppress("DEPRECATION") private inner class TabFragmentPageAdapter : FragmentPagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { diff --git a/app/src/main/res/drawable/ic_select_all.xml b/app/src/main/res/drawable/ic_select_all.xml new file mode 100644 index 000000000..33d0686b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_select_all.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_select_from.xml b/app/src/main/res/drawable/ic_select_from.xml new file mode 100644 index 000000000..3adfe499a --- /dev/null +++ b/app/src/main/res/drawable/ic_select_from.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_select_invert.xml b/app/src/main/res/drawable/ic_select_invert.xml new file mode 100644 index 000000000..dc47d2a61 --- /dev/null +++ b/app/src/main/res/drawable/ic_select_invert.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_content.xml b/app/src/main/res/layout/activity_search_content.xml index 7a6b8751b..3974749e0 100644 --- a/app/src/main/res/layout/activity_search_content.xml +++ b/app/src/main/res/layout/activity_search_content.xml @@ -17,6 +17,7 @@ android:id="@+id/refresh_progress_bar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" android:indeterminate="true" app:layout_constraintTop_toBottomOf="@id/title_bar" /> @@ -32,8 +33,10 @@ @@ -41,13 +44,12 @@ + android:textSize="14sp" + android:textStyle="bold" /> - @@ -8,82 +9,166 @@ + android:layout_height="match_parent" + android:overScrollMode="never" /> - + + + + + + + + + + + + + + + + + + + + android:layout_gravity="bottom" + app:layout_behavior="com.google.android.material.behavior.HideViewOnScrollBehavior"> - + android:layout_gravity="center" + android:orientation="horizontal"> + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:orientation="vertical"> + + - - - - - - - - - + - \ No newline at end of file + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookmark.xml b/app/src/main/res/layout/item_bookmark.xml index df0227ffd..73324356b 100644 --- a/app/src/main/res/layout/item_bookmark.xml +++ b/app/src/main/res/layout/item_bookmark.xml @@ -4,20 +4,22 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingHorizontal="16dp" - android:paddingVertical="8dp" + android:paddingVertical="12dp" android:background="?attr/selectableItemBackground"> + android:layout_height="wrap_content" + android:paddingBottom="2dp" + android:textSize="16sp" + android:textStyle="bold" /> @@ -25,6 +27,7 @@ android:id="@+id/tv_content" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textColor="?attr/colorSecondary" android:textSize="12sp" android:singleLine="true" /> diff --git a/app/src/main/res/layout/item_chapter_list.xml b/app/src/main/res/layout/item_chapter_list.xml index c4d229fb5..51b993747 100644 --- a/app/src/main/res/layout/item_chapter_list.xml +++ b/app/src/main/res/layout/item_chapter_list.xml @@ -31,7 +31,7 @@ android:id="@+id/iv_volume" android:layout_width="4dp" android:layout_height="16dp" - android:layout_marginEnd="4dp" + android:layout_marginEnd="8dp" android:visibility="visible" app:cardBackgroundColor="?attr/colorTertiary" app:strokeWidth="0dp" /> @@ -40,7 +40,7 @@ diff --git a/app/src/main/res/menu/book_toc.xml b/app/src/main/res/menu/book_toc.xml index d8af1015c..4c5cfbda2 100644 --- a/app/src/main/res/menu/book_toc.xml +++ b/app/src/main/res/menu/book_toc.xml @@ -10,6 +10,12 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="always" /> + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac20cd590..3220ff0f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ datastorePreferences = "1.1.7" kotlin = "2.2.10" ksp = "2.2.10-2.0.2" -agp = "8.12.2" +agp = "8.12.3" appcompat = "1.7.1" colorpicker = "1.1.0" commonsText = "1.14.0"