[优化] 新增下载线程调整和新增缓存管理界面,可以管理下载书籍和章节等
This commit is contained in:
14 files changed
+776
-5
No files matched your search
@@ -66,6 +66,7 @@ object PreferKey {
|
|||||||
const val backupPath = "backupUri"
|
const val backupPath = "backupUri"
|
||||||
const val restoreIgnore = "restoreIgnore"
|
const val restoreIgnore = "restoreIgnore"
|
||||||
const val threadCount = "threadCount"
|
const val threadCount = "threadCount"
|
||||||
|
const val cacheBookThreadCount = "cacheBookThreadCount"
|
||||||
const val webPort = "webPort"
|
const val webPort = "webPort"
|
||||||
const val keepLight = "keep_light"
|
const val keepLight = "keep_light"
|
||||||
const val readSliderMode = "read_slider_mode"
|
const val readSliderMode = "read_slider_mode"
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import io.legado.app.help.coil.CoverInterceptor
|
|||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
import io.legado.app.help.http.okHttpClientManga
|
import io.legado.app.help.http.okHttpClientManga
|
||||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||||
|
import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
|
||||||
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
||||||
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
|
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
|
||||||
import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
|
import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
|
||||||
@@ -189,6 +190,7 @@ val appModule = module {
|
|||||||
viewModelOf(::ExploreViewModel)
|
viewModelOf(::ExploreViewModel)
|
||||||
viewModelOf(::RssViewModel)
|
viewModelOf(::RssViewModel)
|
||||||
viewModelOf(::SearchViewModel)
|
viewModelOf(::SearchViewModel)
|
||||||
|
viewModelOf(::BookCacheManageViewModel)
|
||||||
viewModel {
|
viewModel {
|
||||||
BookshelfManageScreenViewModel(
|
BookshelfManageScreenViewModel(
|
||||||
application = get(),
|
application = get(),
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ object CacheBook {
|
|||||||
}.onStart {
|
}.onStart {
|
||||||
postEvent(EventBus.UP_DOWNLOAD_STATE, "")
|
postEvent(EventBus.UP_DOWNLOAD_STATE, "")
|
||||||
updateSummary()
|
updateSummary()
|
||||||
}.onEachParallel(OtherConfig.threadCount.coerceAtLeast(1)) {
|
}.onEachParallel(OtherConfig.cacheBookThreadCount.coerceAtLeast(1)) {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
it.download(this, context)
|
it.download(this, context)
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ object CacheBook {
|
|||||||
val downloadSummary: String
|
val downloadSummary: String
|
||||||
get() {
|
get() {
|
||||||
val stats = collectQueueStats()
|
val stats = collectQueueStats()
|
||||||
return "正在下载:${stats.downloadingCount}|等待中:${stats.waitingCount}|失败:${errorDownloadMap.size}|成功:${successDownloadSet.size}"
|
return "正在下载:${stats.downloadingCount} | 等待中:${stats.waitingCount} | 失败:${errorDownloadMap.size} | 成功:${successDownloadSet.size}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRun: Boolean
|
val isRun: Boolean
|
||||||
@@ -259,6 +259,12 @@ object CacheBook {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun queueCounts(): Pair<Int, Int> = waitDownloadSet.size to onDownloadSet.size
|
fun queueCounts(): Pair<Int, Int> = waitDownloadSet.size to onDownloadSet.size
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun waitingIndices(): Set<Int> = waitDownloadSet.toSet()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun downloadingIndices(): Set<Int> = onDownloadSet.toSet()
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun isRun(): Boolean {
|
fun isRun(): Boolean {
|
||||||
return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty() || isLoading
|
return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty() || isLoading
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class CacheBookService : BaseService() {
|
|||||||
private set
|
private set
|
||||||
}
|
}
|
||||||
|
|
||||||
private val threadCount = OtherConfig.threadCount
|
private val threadCount = OtherConfig.cacheBookThreadCount
|
||||||
private var cachePool =
|
private var cachePool =
|
||||||
Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
||||||
private var downloadJob: Job? = null
|
private var downloadJob: Job? = null
|
||||||
|
|||||||
+443
@@ -0,0 +1,443 @@
|
|||||||
|
package io.legado.app.ui.book.cache.manage
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
|
import io.legado.app.ui.widget.components.AppLinearProgressIndicator
|
||||||
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||||
|
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||||
|
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
|
||||||
|
import io.legado.app.ui.widget.components.card.NormalCard
|
||||||
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
|
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookCacheManageRouteScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
viewModel: BookCacheManageViewModel = koinViewModel()
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.onIntent(BookCacheManageIntent.Initialize)
|
||||||
|
}
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(viewModel) {
|
||||||
|
viewModel.effects.collect { effect ->
|
||||||
|
when (effect) {
|
||||||
|
is BookCacheManageEffect.ShowMessage -> context.toastOnUi(effect.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
BookCacheManageScreen(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onIntent = viewModel::onIntent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun BookCacheManageScreen(
|
||||||
|
state: BookCacheManageUiState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onIntent: (BookCacheManageIntent) -> Unit,
|
||||||
|
) {
|
||||||
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
var pendingDeleteBook by remember { mutableStateOf<BookCacheBookItem?>(null) }
|
||||||
|
var pendingDeleteChapter by remember { mutableStateOf<Pair<BookCacheBookItem, BookCacheChapterItem>?>(null) }
|
||||||
|
var expandedBookUrls by rememberSaveable { mutableStateOf(emptySet<String>()) }
|
||||||
|
val allBooks = state.shelfBooks + state.notShelfBooks
|
||||||
|
val hasRunningDownload = allBooks.any { it.isDownloading }
|
||||||
|
val hasDownloadTarget = allBooks.any { it.chapters.any { chapter -> !chapter.isCached } }
|
||||||
|
|
||||||
|
AppScaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
GlassMediumFlexibleTopAppBar(
|
||||||
|
title = "缓存管理",
|
||||||
|
subtitle = state.downloadSummary.takeIf { it.isNotBlank() },
|
||||||
|
navigationIcon = {
|
||||||
|
TopBarNavigationButton(onClick = onBackClick)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TopBarActionButton(
|
||||||
|
onClick = { onIntent(BookCacheManageIntent.Refresh) },
|
||||||
|
imageVector = Icons.Default.Refresh,
|
||||||
|
contentDescription = "刷新"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (hasRunningDownload || hasDownloadTarget) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
if (hasRunningDownload) {
|
||||||
|
onIntent(BookCacheManageIntent.StopAllDownloads)
|
||||||
|
} else {
|
||||||
|
onIntent(BookCacheManageIntent.StartAllDownloads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = if (hasRunningDownload) Icons.Default.Stop else Icons.Default.Download,
|
||||||
|
contentDescription = if (hasRunningDownload) "停止下载" else "开始下载"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
if (state.isLoading) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = adaptiveContentPadding(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = paddingValues.calculateBottomPadding() + 24.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
cacheSection(
|
||||||
|
title = "书架书籍",
|
||||||
|
emptyText = "没有书架内书籍缓存或下载任务",
|
||||||
|
books = state.shelfBooks,
|
||||||
|
expandedBookUrls = expandedBookUrls,
|
||||||
|
onToggleExpanded = { bookUrl ->
|
||||||
|
expandedBookUrls = expandedBookUrls.toggle(bookUrl)
|
||||||
|
},
|
||||||
|
onIntent = onIntent,
|
||||||
|
onDeleteBook = { pendingDeleteBook = it },
|
||||||
|
onDeleteChapter = { book, chapter -> pendingDeleteChapter = book to chapter }
|
||||||
|
)
|
||||||
|
cacheSection(
|
||||||
|
title = "未在书架",
|
||||||
|
emptyText = "没有未在书架的书籍下载状态",
|
||||||
|
books = state.notShelfBooks,
|
||||||
|
expandedBookUrls = expandedBookUrls,
|
||||||
|
onToggleExpanded = { bookUrl ->
|
||||||
|
expandedBookUrls = expandedBookUrls.toggle(bookUrl)
|
||||||
|
},
|
||||||
|
onIntent = onIntent,
|
||||||
|
onDeleteBook = { pendingDeleteBook = it },
|
||||||
|
onDeleteChapter = { book, chapter -> pendingDeleteChapter = book to chapter }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteBookCacheDialog(
|
||||||
|
item = pendingDeleteBook,
|
||||||
|
onConfirm = { item ->
|
||||||
|
onIntent(BookCacheManageIntent.DeleteBookCache(item.book.bookUrl))
|
||||||
|
pendingDeleteBook = null
|
||||||
|
},
|
||||||
|
onDismiss = { pendingDeleteBook = null }
|
||||||
|
)
|
||||||
|
DeleteChapterCacheDialog(
|
||||||
|
item = pendingDeleteChapter,
|
||||||
|
onConfirm = { book, chapter ->
|
||||||
|
onIntent(
|
||||||
|
BookCacheManageIntent.DeleteChapterCache(
|
||||||
|
book.book.bookUrl,
|
||||||
|
chapter.chapter.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pendingDeleteChapter = null
|
||||||
|
},
|
||||||
|
onDismiss = { pendingDeleteChapter = null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.cacheSection(
|
||||||
|
title: String,
|
||||||
|
emptyText: String,
|
||||||
|
books: List<BookCacheBookItem>,
|
||||||
|
expandedBookUrls: Set<String>,
|
||||||
|
onToggleExpanded: (String) -> Unit,
|
||||||
|
onIntent: (BookCacheManageIntent) -> Unit,
|
||||||
|
onDeleteBook: (BookCacheBookItem) -> Unit,
|
||||||
|
onDeleteChapter: (BookCacheBookItem, BookCacheChapterItem) -> Unit,
|
||||||
|
) {
|
||||||
|
item(key = "$title-header") {
|
||||||
|
AppText(
|
||||||
|
text = title,
|
||||||
|
modifier = Modifier.padding(start = 4.dp, top = 12.dp, bottom = 4.dp),
|
||||||
|
style = LegadoTheme.typography.titleSmallEmphasized,
|
||||||
|
color = LegadoTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (books.isEmpty()) {
|
||||||
|
item(key = "$title-empty") {
|
||||||
|
TextCard(text = emptyText, modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
books.forEach { item ->
|
||||||
|
val bookUrl = item.book.bookUrl
|
||||||
|
val expanded = expandedBookUrls.contains(bookUrl)
|
||||||
|
item(key = "$title-book-$bookUrl") {
|
||||||
|
BookCacheBookCard(
|
||||||
|
item = item,
|
||||||
|
expanded = expanded,
|
||||||
|
onToggleExpanded = { onToggleExpanded(bookUrl) },
|
||||||
|
onIntent = onIntent,
|
||||||
|
onDeleteBook = onDeleteBook,
|
||||||
|
modifier = Modifier.animateItem()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (expanded) {
|
||||||
|
items(
|
||||||
|
items = item.chapters,
|
||||||
|
key = { chapter -> "$title-chapter-$bookUrl-${chapter.chapter.url}" }
|
||||||
|
) { chapter ->
|
||||||
|
BookCacheChapterRow(
|
||||||
|
item = chapter,
|
||||||
|
modifier = Modifier.animateItem(),
|
||||||
|
onDownload = {
|
||||||
|
onIntent(
|
||||||
|
BookCacheManageIntent.DownloadChapter(
|
||||||
|
bookUrl,
|
||||||
|
chapter.chapter.index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDelete = { onDeleteChapter(item, chapter) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookCacheBookCard(
|
||||||
|
item: BookCacheBookItem,
|
||||||
|
expanded: Boolean,
|
||||||
|
onToggleExpanded: () -> Unit,
|
||||||
|
onIntent: (BookCacheManageIntent) -> Unit,
|
||||||
|
onDeleteBook: (BookCacheBookItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val arrowRotation by animateFloatAsState(
|
||||||
|
targetValue = if (expanded) 90f else 0f,
|
||||||
|
label = "BookCacheExpandArrow"
|
||||||
|
)
|
||||||
|
NormalCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
onClick = onToggleExpanded,
|
||||||
|
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.graphicsLayer(rotationZ = arrowRotation)
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
AppText(
|
||||||
|
text = item.book.name,
|
||||||
|
style = LegadoTheme.typography.titleSmallEmphasized,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
AppText(
|
||||||
|
text = item.book.getRealAuthor(),
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextCard(text = "${item.cachedCount}/${item.totalCount}")
|
||||||
|
}
|
||||||
|
AppLinearProgressIndicator(
|
||||||
|
progress = item.progress,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppText(
|
||||||
|
text = "下载中 ${item.downloadingCount} · 等待 ${item.waitingCount} · 失败 ${item.errorCount}",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
SmallTonalIconButton(
|
||||||
|
onClick = { onDeleteBook(item) },
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookCacheChapterRow(
|
||||||
|
item: BookCacheChapterItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onDownload: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
AppText(
|
||||||
|
text = item.chapter.title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = LegadoTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
AppText(
|
||||||
|
text = chapterStatusText(item),
|
||||||
|
maxLines = 1,
|
||||||
|
style = LegadoTheme.typography.labelSmall,
|
||||||
|
color = if (item.isError) {
|
||||||
|
LegadoTheme.colorScheme.error
|
||||||
|
} else if (item.isCached) {
|
||||||
|
LegadoTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SmallTonalIconButton(
|
||||||
|
onClick = onDownload,
|
||||||
|
imageVector = Icons.Default.Download,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
SmallTonalIconButton(
|
||||||
|
onClick = onDelete,
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Set<String>.toggle(value: String): Set<String> {
|
||||||
|
return if (contains(value)) this - value else this + value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chapterStatusText(item: BookCacheChapterItem): String {
|
||||||
|
return when {
|
||||||
|
item.isDownloading -> "下载中"
|
||||||
|
item.isWaiting -> "等待下载"
|
||||||
|
item.isError -> "下载失败"
|
||||||
|
item.isCached -> "已缓存"
|
||||||
|
else -> "未缓存"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeleteBookCacheDialog(
|
||||||
|
item: BookCacheBookItem?,
|
||||||
|
onConfirm: (BookCacheBookItem) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
AppAlertDialog(
|
||||||
|
show = item != null,
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = stringResource(R.string.delete),
|
||||||
|
text = "删除《${item?.book?.name.orEmpty()}》的全部缓存,并从下载队列移除?",
|
||||||
|
confirmText = stringResource(android.R.string.ok),
|
||||||
|
onConfirm = { item?.let(onConfirm) },
|
||||||
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
|
onDismiss = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeleteChapterCacheDialog(
|
||||||
|
item: Pair<BookCacheBookItem, BookCacheChapterItem>?,
|
||||||
|
onConfirm: (BookCacheBookItem, BookCacheChapterItem) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
AppAlertDialog(
|
||||||
|
show = item != null,
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = stringResource(R.string.delete),
|
||||||
|
text = "删除章节缓存:${item?.second?.chapter?.title.orEmpty()}?",
|
||||||
|
confirmText = stringResource(android.R.string.ok),
|
||||||
|
onConfirm = { item?.let { onConfirm(it.first, it.second) } },
|
||||||
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
|
onDismiss = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
+264
@@ -0,0 +1,264 @@
|
|||||||
|
package io.legado.app.ui.book.cache.manage
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import io.legado.app.base.BaseViewModel
|
||||||
|
import io.legado.app.data.dao.BookChapterDao
|
||||||
|
import io.legado.app.data.dao.BookDao
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
||||||
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||||
|
import io.legado.app.help.book.BookHelp
|
||||||
|
import io.legado.app.help.book.isAudio
|
||||||
|
import io.legado.app.help.book.isLocal
|
||||||
|
import io.legado.app.help.book.isNotShelf
|
||||||
|
import io.legado.app.model.CacheBook
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
data class BookCacheManageUiState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val shelfBooks: List<BookCacheBookItem> = emptyList(),
|
||||||
|
val notShelfBooks: List<BookCacheBookItem> = emptyList(),
|
||||||
|
val downloadSummary: String = CacheBook.downloadSummary,
|
||||||
|
val version: Long = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookCacheBookItem(
|
||||||
|
val book: Book,
|
||||||
|
val chapters: List<BookCacheChapterItem>,
|
||||||
|
val cachedCount: Int,
|
||||||
|
val waitingCount: Int,
|
||||||
|
val downloadingCount: Int,
|
||||||
|
val errorCount: Int,
|
||||||
|
val isNotShelf: Boolean,
|
||||||
|
) {
|
||||||
|
val totalCount: Int get() = chapters.size
|
||||||
|
val progress: Float get() = if (totalCount == 0) 0f else cachedCount.toFloat() / totalCount
|
||||||
|
val isDownloading: Boolean get() = waitingCount > 0 || downloadingCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BookCacheChapterItem(
|
||||||
|
val chapter: BookChapter,
|
||||||
|
val isCached: Boolean,
|
||||||
|
val isWaiting: Boolean,
|
||||||
|
val isDownloading: Boolean,
|
||||||
|
val isError: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface BookCacheManageIntent {
|
||||||
|
data object Initialize : BookCacheManageIntent
|
||||||
|
data object Refresh : BookCacheManageIntent
|
||||||
|
data object StartAllDownloads : BookCacheManageIntent
|
||||||
|
data object StopAllDownloads : BookCacheManageIntent
|
||||||
|
data class DeleteBookCache(val bookUrl: String) : BookCacheManageIntent
|
||||||
|
data class DownloadChapter(val bookUrl: String, val chapterIndex: Int) : BookCacheManageIntent
|
||||||
|
data class DeleteChapterCache(val bookUrl: String, val chapterUrl: String) : BookCacheManageIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface BookCacheManageEffect {
|
||||||
|
data class ShowMessage(val message: String) : BookCacheManageEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookCacheManageViewModel(
|
||||||
|
application: Application,
|
||||||
|
private val bookDao: BookDao,
|
||||||
|
private val bookChapterDao: BookChapterDao,
|
||||||
|
private val cacheBookChaptersUseCase: CacheBookChaptersUseCase,
|
||||||
|
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
||||||
|
) : BaseViewModel(application) {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(BookCacheManageUiState())
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _effects = MutableSharedFlow<BookCacheManageEffect>(extraBufferCapacity = 16)
|
||||||
|
val effects = _effects.asSharedFlow()
|
||||||
|
|
||||||
|
private var observeJob: Job? = null
|
||||||
|
private val chapterCache = hashMapOf<String, List<BookChapter>>()
|
||||||
|
|
||||||
|
fun onIntent(intent: BookCacheManageIntent) {
|
||||||
|
when (intent) {
|
||||||
|
BookCacheManageIntent.Initialize -> initialize()
|
||||||
|
BookCacheManageIntent.Refresh -> reload()
|
||||||
|
BookCacheManageIntent.StartAllDownloads -> startAllDownloads()
|
||||||
|
BookCacheManageIntent.StopAllDownloads -> stopAllDownloads()
|
||||||
|
is BookCacheManageIntent.DeleteBookCache -> deleteBookCache(intent.bookUrl)
|
||||||
|
is BookCacheManageIntent.DownloadChapter -> downloadChapter(
|
||||||
|
intent.bookUrl,
|
||||||
|
intent.chapterIndex
|
||||||
|
)
|
||||||
|
is BookCacheManageIntent.DeleteChapterCache -> deleteChapterCache(
|
||||||
|
intent.bookUrl,
|
||||||
|
intent.chapterUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
if (observeJob != null) return
|
||||||
|
observeJob = viewModelScope.launch {
|
||||||
|
bookDao.flowAll().collect {
|
||||||
|
reload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
CacheBook.cacheSuccessFlow.collect {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
CacheBook.downloadingIndicesFlow.collect {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
CacheBook.downloadErrorFlow.collect {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
CacheBook.downloadSummaryFlow.collect { summary ->
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(downloadSummary = summary, version = it.version + 1)
|
||||||
|
}
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reload(books: List<Book>? = null) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val sourceBooks = books ?: bookDao.all
|
||||||
|
val items = sourceBooks
|
||||||
|
.filterNot { it.isLocal || it.isAudio }
|
||||||
|
.mapNotNull { book -> buildBookItem(book) }
|
||||||
|
.filter { item ->
|
||||||
|
item.cachedCount > 0 || item.isDownloading || item.errorCount > 0
|
||||||
|
}
|
||||||
|
.sortedWith(compareByDescending<BookCacheBookItem> { it.isDownloading }
|
||||||
|
.thenByDescending { it.cachedCount }
|
||||||
|
.thenBy { it.book.name })
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
shelfBooks = items.filterNot { item -> item.isNotShelf },
|
||||||
|
notShelfBooks = items.filter { item -> item.isNotShelf },
|
||||||
|
downloadSummary = CacheBook.downloadSummary,
|
||||||
|
version = it.version + 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildBookItem(book: Book): BookCacheBookItem? {
|
||||||
|
val chapters = chapterCache.getOrPut(book.bookUrl) {
|
||||||
|
bookChapterDao.getChapterList(book.bookUrl)
|
||||||
|
}
|
||||||
|
val cacheFiles = BookHelp.getChapterFiles(book)
|
||||||
|
val model = CacheBook.cacheBookMap[book.bookUrl]
|
||||||
|
val waitingIndices = model?.waitingIndices().orEmpty()
|
||||||
|
val downloadingIndices = model?.downloadingIndices().orEmpty()
|
||||||
|
val errorIndices = CacheBook.downloadErrorFlow.value
|
||||||
|
.takeIf { it.first == book.bookUrl }
|
||||||
|
?.second
|
||||||
|
.orEmpty()
|
||||||
|
if (chapters.isEmpty() && cacheFiles.isEmpty() && model == null && !book.isNotShelf) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val chapterItems = chapters.map { chapter ->
|
||||||
|
BookCacheChapterItem(
|
||||||
|
chapter = chapter,
|
||||||
|
isCached = cacheFiles.contains(chapter.getFileName()) || chapter.isVolume,
|
||||||
|
isWaiting = waitingIndices.contains(chapter.index),
|
||||||
|
isDownloading = downloadingIndices.contains(chapter.index),
|
||||||
|
isError = errorIndices.contains(chapter.index),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return BookCacheBookItem(
|
||||||
|
book = book,
|
||||||
|
chapters = chapterItems,
|
||||||
|
cachedCount = chapterItems.count { it.isCached },
|
||||||
|
waitingCount = waitingIndices.size,
|
||||||
|
downloadingCount = downloadingIndices.size,
|
||||||
|
errorCount = errorIndices.size,
|
||||||
|
isNotShelf = book.isNotShelf,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopAllDownloads() {
|
||||||
|
CacheBook.stop(context)
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startAllDownloads() {
|
||||||
|
val items = uiState.value.shelfBooks + uiState.value.notShelfBooks
|
||||||
|
execute {
|
||||||
|
items.sumOf { item ->
|
||||||
|
val chapterIndices = item.chapters
|
||||||
|
.filterNot { it.isCached || it.isWaiting || it.isDownloading }
|
||||||
|
.map { it.chapter.index }
|
||||||
|
cacheBookChaptersUseCase.execute(item.book.bookUrl, chapterIndices)
|
||||||
|
}
|
||||||
|
}.onSuccess { count ->
|
||||||
|
if (count > 0) {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("已加入缓存队列: $count 章"))
|
||||||
|
} else {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("没有可缓存的章节"))
|
||||||
|
}
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("加入缓存队列失败\n${it.localizedMessage}"))
|
||||||
|
}.onFinally {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteBookCache(bookUrl: String) {
|
||||||
|
CacheBook.remove(context, bookUrl)
|
||||||
|
execute {
|
||||||
|
clearBookCacheUseCase.execute(bookUrl)
|
||||||
|
}.onSuccess {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("缓存已删除"))
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除缓存失败\n${it.localizedMessage}"))
|
||||||
|
}.onFinally {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadChapter(bookUrl: String, chapterIndex: Int) {
|
||||||
|
execute {
|
||||||
|
cacheBookChaptersUseCase.execute(bookUrl, listOf(chapterIndex))
|
||||||
|
}.onSuccess {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("章节已加入缓存队列"))
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("章节缓存失败\n${it.localizedMessage}"))
|
||||||
|
}.onFinally {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteChapterCache(bookUrl: String, chapterUrl: String) {
|
||||||
|
val book = bookDao.getBook(bookUrl) ?: return
|
||||||
|
val chapter = chapterCache[bookUrl]?.firstOrNull { it.url == chapterUrl } ?: return
|
||||||
|
execute {
|
||||||
|
BookHelp.delContent(book, chapter)
|
||||||
|
}.onSuccess {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("章节缓存已删除"))
|
||||||
|
}.onError {
|
||||||
|
_effects.tryEmit(BookCacheManageEffect.ShowMessage("删除章节缓存失败\n${it.localizedMessage}"))
|
||||||
|
}.onFinally {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,11 @@ object OtherConfig {
|
|||||||
16
|
16
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cacheBookThreadCount by prefDelegate(
|
||||||
|
PreferKey.cacheBookThreadCount,
|
||||||
|
16
|
||||||
|
)
|
||||||
|
|
||||||
var processText by prefDelegate(
|
var processText by prefDelegate(
|
||||||
PreferKey.processText,
|
PreferKey.processText,
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -372,6 +372,15 @@ fun OtherConfigScreen(
|
|||||||
onValueChange = { OtherConfig.threadCount = it.toInt() }
|
onValueChange = { OtherConfig.threadCount = it.toInt() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SliderSettingItem(
|
||||||
|
title = stringResource(R.string.cache_book_threads_num_title),
|
||||||
|
description = stringResource(R.string.cache_book_threads_num_summary),
|
||||||
|
value = OtherConfig.cacheBookThreadCount.toFloat(),
|
||||||
|
defaultValue = 16f,
|
||||||
|
valueRange = 1f..256f,
|
||||||
|
onValueChange = { OtherConfig.cacheBookThreadCount = it.toInt() }
|
||||||
|
)
|
||||||
|
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
title = stringResource(R.string.add_to_text_context_menu_t),
|
title = stringResource(R.string.add_to_text_context_menu_t),
|
||||||
description = stringResource(R.string.add_to_text_context_menu_s),
|
description = stringResource(R.string.add_to_text_context_menu_s),
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import io.legado.app.lib.dialogs.alert
|
|||||||
import io.legado.app.service.WebService
|
import io.legado.app.service.WebService
|
||||||
import io.legado.app.ui.about.CrashLogsDialog
|
import io.legado.app.ui.about.CrashLogsDialog
|
||||||
import io.legado.app.ui.about.UpdateDialog
|
import io.legado.app.ui.about.UpdateDialog
|
||||||
|
import io.legado.app.ui.book.cache.manage.BookCacheManageRouteScreen
|
||||||
import io.legado.app.ui.book.info.BookInfoActivity
|
import io.legado.app.ui.book.info.BookInfoActivity
|
||||||
import io.legado.app.ui.book.import.local.ImportBookScreen
|
import io.legado.app.ui.book.import.local.ImportBookScreen
|
||||||
import io.legado.app.ui.book.import.remote.RemoteBookScreen
|
import io.legado.app.ui.book.import.remote.RemoteBookScreen
|
||||||
@@ -93,6 +94,7 @@ open class MainActivity : BaseComposeActivity() {
|
|||||||
private const val ROUTE_IMPORT_LOCAL = "import/local"
|
private const val ROUTE_IMPORT_LOCAL = "import/local"
|
||||||
private const val ROUTE_IMPORT_REMOTE = "import/remote"
|
private const val ROUTE_IMPORT_REMOTE = "import/remote"
|
||||||
private const val ROUTE_CACHE = "cache"
|
private const val ROUTE_CACHE = "cache"
|
||||||
|
private const val ROUTE_BOOK_CACHE_MANAGE = "book/cache/manage"
|
||||||
private const val ROUTE_SEARCH = "search"
|
private const val ROUTE_SEARCH = "search"
|
||||||
private const val ROUTE_RSS_SORT = "rss/sort"
|
private const val ROUTE_RSS_SORT = "rss/sort"
|
||||||
private const val ROUTE_RSS_READ = "rss/read"
|
private const val ROUTE_RSS_READ = "rss/read"
|
||||||
@@ -176,6 +178,12 @@ open class MainActivity : BaseComposeActivity() {
|
|||||||
groupId: Long = -1L
|
groupId: Long = -1L
|
||||||
): Intent = createBookshelfManageScreenIntent(context, groupId)
|
): Intent = createBookshelfManageScreenIntent(context, groupId)
|
||||||
|
|
||||||
|
fun createBookCacheManageIntent(context: Context): Intent {
|
||||||
|
return createLauncherIntent(context).apply {
|
||||||
|
putExtra(EXTRA_START_ROUTE, ROUTE_BOOK_CACHE_MANAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createSearchIntent(
|
fun createSearchIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
key: String? = null,
|
key: String? = null,
|
||||||
@@ -236,6 +244,9 @@ open class MainActivity : BaseComposeActivity() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
private data class MainRouteCache(val groupId: Long) : MainRoute
|
private data class MainRouteCache(val groupId: Long) : MainRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data object MainRouteBookCacheManage : MainRoute
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class MainRouteSearch(
|
private data class MainRouteSearch(
|
||||||
val key: String?,
|
val key: String?,
|
||||||
@@ -400,6 +411,9 @@ open class MainActivity : BaseComposeActivity() {
|
|||||||
onNavigateToCache = { groupId ->
|
onNavigateToCache = { groupId ->
|
||||||
navigateToRoute(backStack, MainRouteCache(groupId))
|
navigateToRoute(backStack, MainRouteCache(groupId))
|
||||||
},
|
},
|
||||||
|
onNavigateToBookCacheManage = {
|
||||||
|
navigateToRoute(backStack, MainRouteBookCacheManage)
|
||||||
|
},
|
||||||
onNavigateToRssSort = { sourceUrl, sortUrl, key ->
|
onNavigateToRssSort = { sourceUrl, sortUrl, key ->
|
||||||
navigateToRoute(
|
navigateToRoute(
|
||||||
backStack,
|
backStack,
|
||||||
@@ -474,6 +488,12 @@ open class MainActivity : BaseComposeActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<MainRouteBookCacheManage> {
|
||||||
|
BookCacheManageRouteScreen(
|
||||||
|
onBackClick = { navigateBack(backStack) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
entry<MainRouteSearch> { route ->
|
entry<MainRouteSearch> { route ->
|
||||||
val searchViewModel = koinViewModel<SearchViewModel>()
|
val searchViewModel = koinViewModel<SearchViewModel>()
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
@@ -590,7 +610,8 @@ open class MainActivity : BaseComposeActivity() {
|
|||||||
|
|
||||||
MainRouteImportLocal,
|
MainRouteImportLocal,
|
||||||
MainRouteImportRemote,
|
MainRouteImportRemote,
|
||||||
is MainRouteCache -> {
|
is MainRouteCache,
|
||||||
|
MainRouteBookCacheManage -> {
|
||||||
if (currentRoute == MainRouteHome) {
|
if (currentRoute == MainRouteHome) {
|
||||||
backStack.add(route)
|
backStack.add(route)
|
||||||
} else {
|
} else {
|
||||||
@@ -801,6 +822,7 @@ open class MainActivity : BaseComposeActivity() {
|
|||||||
ROUTE_IMPORT_LOCAL -> MainRouteImportLocal
|
ROUTE_IMPORT_LOCAL -> MainRouteImportLocal
|
||||||
ROUTE_IMPORT_REMOTE -> MainRouteImportRemote
|
ROUTE_IMPORT_REMOTE -> MainRouteImportRemote
|
||||||
ROUTE_CACHE -> MainRouteCache(intent?.getLongExtra(EXTRA_CACHE_GROUP_ID, -1L) ?: -1L)
|
ROUTE_CACHE -> MainRouteCache(intent?.getLongExtra(EXTRA_CACHE_GROUP_ID, -1L) ?: -1L)
|
||||||
|
ROUTE_BOOK_CACHE_MANAGE -> MainRouteBookCacheManage
|
||||||
ROUTE_SEARCH -> MainRouteSearch(
|
ROUTE_SEARCH -> MainRouteSearch(
|
||||||
key = intent?.getStringExtra(EXTRA_SEARCH_KEY),
|
key = intent?.getStringExtra(EXTRA_SEARCH_KEY),
|
||||||
scopeRaw = intent?.getStringExtra(EXTRA_SEARCH_SCOPE)
|
scopeRaw = intent?.getStringExtra(EXTRA_SEARCH_SCOPE)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import io.legado.app.ui.main.bookshelf.BookshelfScreen
|
|||||||
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||||
import io.legado.app.ui.main.explore.ExploreScreen
|
import io.legado.app.ui.main.explore.ExploreScreen
|
||||||
import io.legado.app.ui.main.my.MyScreen
|
import io.legado.app.ui.main.my.MyScreen
|
||||||
|
import io.legado.app.ui.main.my.PrefClickEvent
|
||||||
import io.legado.app.ui.main.rss.RssScreen
|
import io.legado.app.ui.main.rss.RssScreen
|
||||||
import io.legado.app.ui.theme.regularHazeEffect
|
import io.legado.app.ui.theme.regularHazeEffect
|
||||||
import io.legado.app.ui.widget.components.AppNavigationBar
|
import io.legado.app.ui.widget.components.AppNavigationBar
|
||||||
@@ -93,6 +94,7 @@ fun MainScreen(
|
|||||||
onNavigateToRemoteImport: () -> Unit,
|
onNavigateToRemoteImport: () -> Unit,
|
||||||
onNavigateToLocalImport: () -> Unit,
|
onNavigateToLocalImport: () -> Unit,
|
||||||
onNavigateToCache: (Long) -> Unit,
|
onNavigateToCache: (Long) -> Unit,
|
||||||
|
onNavigateToBookCacheManage: () -> Unit,
|
||||||
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
||||||
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit
|
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -397,7 +399,11 @@ fun MainScreen(
|
|||||||
viewModel = koinViewModel(),
|
viewModel = koinViewModel(),
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onNavigate = { event ->
|
onNavigate = { event ->
|
||||||
viewModel.onPrefClickEvent(context, event)
|
if (event == PrefClickEvent.OpenBookCacheManage) {
|
||||||
|
onNavigateToBookCacheManage()
|
||||||
|
} else {
|
||||||
|
viewModel.onPrefClickEvent(context, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
|||||||
import androidx.compose.material.icons.automirrored.filled.Rule
|
import androidx.compose.material.icons.automirrored.filled.Rule
|
||||||
import androidx.compose.material.icons.filled.Bookmark
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.FindReplace
|
import androidx.compose.material.icons.filled.FindReplace
|
||||||
import androidx.compose.material.icons.filled.Folder
|
import androidx.compose.material.icons.filled.Folder
|
||||||
import androidx.compose.material.icons.filled.History
|
import androidx.compose.material.icons.filled.History
|
||||||
@@ -173,6 +174,13 @@ fun MyScreen(
|
|||||||
onNavigate(PrefClickEvent.StartActivity(ReadRecordActivity::class.java))
|
onNavigate(PrefClickEvent.StartActivity(ReadRecordActivity::class.java))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "缓存管理",
|
||||||
|
imageVector = Icons.Default.Download,
|
||||||
|
onClick = {
|
||||||
|
onNavigate(PrefClickEvent.OpenBookCacheManage)
|
||||||
|
}
|
||||||
|
)
|
||||||
ClickableSettingItem(
|
ClickableSettingItem(
|
||||||
title = stringResource(R.string.file_manage),
|
title = stringResource(R.string.file_manage),
|
||||||
imageVector = Icons.Default.Folder,
|
imageVector = Icons.Default.Folder,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ sealed class PrefClickEvent {
|
|||||||
data class CopyUrl(val url: String) : PrefClickEvent()
|
data class CopyUrl(val url: String) : PrefClickEvent()
|
||||||
data class ShowMd(val title: String, val path: String) : PrefClickEvent()
|
data class ShowMd(val title: String, val path: String) : PrefClickEvent()
|
||||||
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
||||||
|
object OpenBookCacheManage : PrefClickEvent()
|
||||||
object ToggleWebService : PrefClickEvent()
|
object ToggleWebService : PrefClickEvent()
|
||||||
object ExitApp : PrefClickEvent()
|
object ExitApp : PrefClickEvent()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,6 +245,8 @@
|
|||||||
<string name="download_count">%1$d/%2$d</string>
|
<string name="download_count">%1$d/%2$d</string>
|
||||||
<string name="import_select_book">导入选择书籍</string>
|
<string name="import_select_book">导入选择书籍</string>
|
||||||
<string name="threads_num_title">更新和搜索线程数</string>
|
<string name="threads_num_title">更新和搜索线程数</string>
|
||||||
|
<string name="cache_book_threads_num_title">书籍缓存下载线程数</string>
|
||||||
|
<string name="cache_book_threads_num_summary">控制离线缓存章节下载的并发数量</string>
|
||||||
<string name="change_icon">切换图标</string>
|
<string name="change_icon">切换图标</string>
|
||||||
<string name="remove_from_bookshelf">已在书架</string>
|
<string name="remove_from_bookshelf">已在书架</string>
|
||||||
<string name="already_in_bookshelf">已在书架</string>
|
<string name="already_in_bookshelf">已在书架</string>
|
||||||
|
|||||||
@@ -248,6 +248,8 @@
|
|||||||
<string name="download_count">Downloaded %1$d/%2$d</string>
|
<string name="download_count">Downloaded %1$d/%2$d</string>
|
||||||
<string name="import_select_book">Import selected book(s)</string>
|
<string name="import_select_book">Import selected book(s)</string>
|
||||||
<string name="threads_num_title">Number of Concurrent Tasks</string>
|
<string name="threads_num_title">Number of Concurrent Tasks</string>
|
||||||
|
<string name="cache_book_threads_num_title">Cache Download Threads</string>
|
||||||
|
<string name="cache_book_threads_num_summary">Controls concurrent chapter downloads for offline book cache</string>
|
||||||
<string name="change_icon">Change Icon</string>
|
<string name="change_icon">Change Icon</string>
|
||||||
<string name="remove_from_bookshelf">In library</string>
|
<string name="remove_from_bookshelf">In library</string>
|
||||||
<string name="already_in_bookshelf">Already in Bookshelf</string>
|
<string name="already_in_bookshelf">Already in Bookshelf</string>
|
||||||
|
|||||||
Reference in new issue
Block a user