修复封面缓存没有一天是命中导致的启动时都会向网络请求封面的问题
This commit is contained in:
@@ -13,98 +13,152 @@ import coil.request.Options
|
||||
import io.legado.app.data.entities.BaseSource
|
||||
import io.legado.app.model.ReadManga
|
||||
import io.legado.app.utils.ImageUtils
|
||||
import io.legado.app.utils.isWifiConnect
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.Buffer
|
||||
import splitties.init.appCtx
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class CoverFetcher(
|
||||
private val url: String,
|
||||
private val options: Options,
|
||||
private val callFactory: Call.Factory
|
||||
private val callFactory: Call.Factory,
|
||||
private val loadOnlyWifi: Boolean,
|
||||
) : Fetcher {
|
||||
|
||||
companion object {
|
||||
/** Tag applied to cover requests so [cacheControlInterceptor] can identify them. */
|
||||
val COVER_REQUEST_TAG = Unit
|
||||
|
||||
private const val FAIL_CACHE_TTL_MS = 5 * 60 * 1000L // 5 minutes
|
||||
|
||||
/** URL -> failure timestamp. Prevents infinite retries for permanently broken URLs. */
|
||||
private val failCache = ConcurrentHashMap<String, Long>()
|
||||
|
||||
fun isFailed(url: String): Boolean {
|
||||
val ts = failCache[url] ?: return false
|
||||
if (System.currentTimeMillis() - ts > FAIL_CACHE_TTL_MS) {
|
||||
failCache.remove(url)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun markFailed(url: String) {
|
||||
failCache[url] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun clearFailCache() {
|
||||
failCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetch(): FetchResult {
|
||||
val source = options.tags.tag<BaseSource>()
|
||||
val isManga = options.parameters.value("manga") as? Boolean == true
|
||||
|
||||
val bytes = if (url.startsWith("data:", true)) {
|
||||
if (url.startsWith("data:", true)) {
|
||||
val base64Data = url.substringAfter("base64,", "")
|
||||
if (base64Data.isEmpty()) {
|
||||
throw IOException("Invalid data URI")
|
||||
}
|
||||
Base64.decode(base64Data, Base64.DEFAULT)
|
||||
} else {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.headers(options.headers)
|
||||
.build()
|
||||
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
callFactory.newCall(request).execute()
|
||||
}
|
||||
val body = response.body
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
body.close()
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
val b = body.use { body ->
|
||||
body.bytes()
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
if (ImageUtils.skipDecode(source, !isManga)) {
|
||||
val bytes = Base64.decode(base64Data, Base64.DEFAULT)
|
||||
return SourceResult(
|
||||
source = ImageSource(source = Buffer().write(bytes), context = options.context),
|
||||
mimeType = null,
|
||||
dataSource = if (url.startsWith(
|
||||
"data:",
|
||||
true
|
||||
)
|
||||
) DataSource.MEMORY else DataSource.NETWORK
|
||||
dataSource = DataSource.MEMORY
|
||||
)
|
||||
}
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val decodedBytes = if (isManga) {
|
||||
ImageUtils.decode(url, bytes, false, source, ReadManga.book)
|
||||
} else {
|
||||
ImageUtils.decode(url, bytes, true, source)
|
||||
if (loadOnlyWifi && !appCtx.isWifiConnect) {
|
||||
throw IOException("WiFi not available, loadOnlyWifi enabled")
|
||||
}
|
||||
|
||||
if (isFailed(url)) {
|
||||
throw IOException("URL previously failed, skipping: $url")
|
||||
}
|
||||
|
||||
// Try OkHttp HTTP cache: FORCE_CACHE serves from cache or returns 504 on miss
|
||||
val cacheRequest = Request.Builder()
|
||||
.url(url)
|
||||
.headers(options.headers)
|
||||
.cacheControl(CacheControl.FORCE_CACHE)
|
||||
.build()
|
||||
|
||||
var fromCache = false
|
||||
val rawBytes = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val cacheResponse = callFactory.newCall(cacheRequest).execute()
|
||||
if (cacheResponse.isSuccessful) {
|
||||
fromCache = true
|
||||
cacheResponse.body.use { it.bytes() }
|
||||
} else {
|
||||
cacheResponse.close()
|
||||
// Cache miss, fetch from network
|
||||
val networkRequest = Request.Builder()
|
||||
.url(url)
|
||||
.headers(options.headers)
|
||||
.tag(COVER_REQUEST_TAG)
|
||||
.cacheControl(
|
||||
CacheControl.Builder()
|
||||
.maxAge(30, TimeUnit.DAYS)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
val networkResponse = callFactory.newCall(networkRequest).execute()
|
||||
val body = networkResponse.body
|
||||
if (!networkResponse.isSuccessful) {
|
||||
body.close()
|
||||
throw HttpException(networkResponse)
|
||||
}
|
||||
body.use { it.bytes() }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
markFailed(url)
|
||||
throw e
|
||||
}
|
||||
|
||||
// Decrypt if needed (applies to both cached and network bytes)
|
||||
val decodedBytes = if (ImageUtils.skipDecode(source, !isManga)) {
|
||||
rawBytes
|
||||
} else {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (isManga) {
|
||||
ImageUtils.decode(url, rawBytes, false, source, ReadManga.book)
|
||||
} else {
|
||||
ImageUtils.decode(url, rawBytes, true, source)
|
||||
}
|
||||
} ?: throw IOException("图片解密失败")
|
||||
|
||||
SourceResult(
|
||||
source = ImageSource(
|
||||
source = Buffer().write(decodedBytes),
|
||||
context = options.context
|
||||
),
|
||||
mimeType = null,
|
||||
dataSource = if (url.startsWith(
|
||||
"data:",
|
||||
true
|
||||
)
|
||||
) DataSource.MEMORY else DataSource.NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
return SourceResult(
|
||||
source = ImageSource(source = Buffer().write(decodedBytes), context = options.context),
|
||||
mimeType = null,
|
||||
dataSource = if (fromCache) DataSource.DISK else DataSource.NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val okHttpClientManga: OkHttpClient
|
||||
private val okHttpClientManga: OkHttpClient,
|
||||
) : Fetcher.Factory<Uri> {
|
||||
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||
val scheme = data.scheme
|
||||
if (scheme != "http" && scheme != "https" && scheme != "data") return null
|
||||
|
||||
val isManga = options.parameters.value("manga") as? Boolean == true
|
||||
val loadOnlyWifi = options.parameters.value("loadOnlyWifi") as? Boolean == true
|
||||
val client = if (isManga) okHttpClientManga else okHttpClient
|
||||
|
||||
return CoverFetcher(data.toString(), options, client)
|
||||
return CoverFetcher(data.toString(), options, client, loadOnlyWifi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,32 @@ import coil.request.ImageResult
|
||||
import io.legado.app.data.entities.BaseSource
|
||||
import io.legado.app.help.source.SourceHelp
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.utils.isWifiConnect
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
|
||||
class CoverInterceptor : Interceptor {
|
||||
|
||||
companion object {
|
||||
private const val RESOLVED_URL_CACHE_MAX_SIZE = 100
|
||||
|
||||
/** LRU cache: "$url|$sourceOrigin" -> Pair(resolvedUrl, headers) */
|
||||
private val resolvedUrlCache = object : LinkedHashMap<String, Pair<String, Map<String, String>>>(
|
||||
16, 0.75f, true
|
||||
) {
|
||||
override fun removeEldestEntry(
|
||||
eldest: MutableMap.MutableEntry<String, Pair<String, Map<String, String>>>?
|
||||
): Boolean {
|
||||
return size > RESOLVED_URL_CACHE_MAX_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
fun clearResolvedUrlCache() {
|
||||
synchronized(resolvedUrlCache) {
|
||||
resolvedUrlCache.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val request = chain.request
|
||||
val data = request.data
|
||||
@@ -19,8 +39,17 @@ class CoverInterceptor : Interceptor {
|
||||
val sourceOrigin = request.parameters.value("sourceOrigin") as? String
|
||||
val source = sourceOrigin?.let { SourceHelp.getSource(it) }
|
||||
|
||||
val (finalUrl, headers) = withContext(Dispatchers.IO) {
|
||||
val cacheKey = "$data|$sourceOrigin"
|
||||
val cached = synchronized(resolvedUrlCache) {
|
||||
resolvedUrlCache[cacheKey]
|
||||
}
|
||||
|
||||
val (finalUrl, headers) = cached ?: withContext(Dispatchers.IO) {
|
||||
AnalyzeUrl(data, source = source).getUrlAndHeaders()
|
||||
}.also { result ->
|
||||
synchronized(resolvedUrlCache) {
|
||||
resolvedUrlCache[cacheKey] = result
|
||||
}
|
||||
}
|
||||
|
||||
val newRequest = request.newBuilder()
|
||||
@@ -37,4 +66,4 @@ class CoverInterceptor : Interceptor {
|
||||
}
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.doOnDetach
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
@@ -92,7 +92,7 @@ abstract class BaseReadBookActivity :
|
||||
setOrientation()
|
||||
upLayoutInDisplayCutoutMode()
|
||||
super.onCreate(savedInstanceState)
|
||||
binding.navigationBar.doOnDetach {
|
||||
binding.navigationBar.doOnAttach {
|
||||
binding.navigationBar.setOnApplyWindowInsetsListenerCompat { view, windowInsets ->
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.updateLayoutParams {
|
||||
@@ -243,13 +243,14 @@ abstract class BaseReadBookActivity :
|
||||
|
||||
fun upNavigationBarColor() {
|
||||
upNavigationBar()
|
||||
when {
|
||||
binding.readMenu.isVisible -> window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurfaceContainer))
|
||||
binding.searchMenu.bottomMenuVisible -> window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurface))
|
||||
bottomDialog > 0 -> window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurface))
|
||||
//!AppConfig.immNavigationBar -> super.upNavigationBarColor()
|
||||
else -> window.setNavigationBarColorAuto(ReadBookConfig.bgMeanColor)
|
||||
val navColor = when {
|
||||
binding.readMenu.isVisible -> themeColor(com.google.android.material.R.attr.colorSurfaceContainer)
|
||||
binding.searchMenu.bottomMenuVisible -> themeColor(com.google.android.material.R.attr.colorSurface)
|
||||
bottomDialog > 0 -> themeColor(com.google.android.material.R.attr.colorSurface)
|
||||
else -> ReadBookConfig.bgMeanColor
|
||||
}
|
||||
window.setNavigationBarColorAuto(navColor)
|
||||
binding.navigationBar.setBackgroundColor(navColor)
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
|
||||
@@ -25,7 +25,8 @@ fun ConfigNavScreen(
|
||||
onNavigateToRead: () -> Unit,
|
||||
onNavigateToCover: () -> Unit,
|
||||
onNavigateToTheme: () -> Unit,
|
||||
onNavigateToBackup: () -> Unit
|
||||
onNavigateToBackup: () -> Unit,
|
||||
onNavigateToDownloadCache: () -> Unit
|
||||
) {
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
|
||||
@@ -66,6 +67,10 @@ fun ConfigNavScreen(
|
||||
title = stringResource(R.string.cover_config),
|
||||
onClick = onNavigateToCover
|
||||
)
|
||||
ClickableSettingItem(
|
||||
title = stringResource(R.string.download_cache_config),
|
||||
onClick = onNavigateToDownloadCache
|
||||
)
|
||||
ClickableSettingItem(
|
||||
title = stringResource(R.string.backup_restore),
|
||||
onClick = onNavigateToBackup
|
||||
|
||||
@@ -7,5 +7,6 @@ object ConfigTag {
|
||||
const val BACKUP_CONFIG = "backupConfig"
|
||||
const val COVER_CONFIG = "coverConfig"
|
||||
const val READ_CONFIG = "readConfig"
|
||||
const val DOWNLOAD_CACHE_CONFIG = "downloadCacheConfig"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.legado.app.ui.config.downloadCacheConfig
|
||||
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.ui.config.prefDelegate
|
||||
|
||||
object DownloadCacheConfig {
|
||||
|
||||
var bitmapCacheSize by prefDelegate(
|
||||
PreferKey.bitmapCacheSize,
|
||||
50
|
||||
)
|
||||
|
||||
var imageRetainNum by prefDelegate(
|
||||
PreferKey.imageRetainNum,
|
||||
0
|
||||
)
|
||||
|
||||
var preDownloadNum by prefDelegate(
|
||||
PreferKey.preDownloadNum,
|
||||
10
|
||||
)
|
||||
|
||||
var threadCount by prefDelegate(
|
||||
PreferKey.threadCount,
|
||||
16
|
||||
)
|
||||
|
||||
var cacheBookThreadCount by prefDelegate(
|
||||
PreferKey.cacheBookThreadCount,
|
||||
16
|
||||
)
|
||||
|
||||
private var _userAgent by prefDelegate(
|
||||
PreferKey.userAgent,
|
||||
""
|
||||
)
|
||||
|
||||
var userAgent: String
|
||||
get() = _userAgent.ifBlank {
|
||||
defaultUserAgent
|
||||
}
|
||||
set(value) {
|
||||
_userAgent = value
|
||||
}
|
||||
|
||||
var cronetEnable by prefDelegate(
|
||||
PreferKey.cronet,
|
||||
false
|
||||
)
|
||||
|
||||
private val defaultUserAgent: String
|
||||
get() = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) " +
|
||||
"Chrome/${BuildConfig.Cronet_Main_Version} Safari/537.36"
|
||||
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package io.legado.app.ui.config.downloadCacheConfig
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.model.CacheBook
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.InputSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DownloadCacheConfigScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: DownloadCacheConfigViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
|
||||
var showClearBookCacheDialog by remember { mutableStateOf(false) }
|
||||
var showShrinkDbDialog by remember { mutableStateOf(false) }
|
||||
var showClearCoverCacheDialog by remember { mutableStateOf(false) }
|
||||
var showClearMangaCacheDialog by remember { mutableStateOf(false) }
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
GlassMediumFlexibleTopAppBar(
|
||||
title = stringResource(R.string.download_cache_config),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(onClick = onBackClick)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = adaptiveContentPadding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp
|
||||
)
|
||||
) {
|
||||
item {
|
||||
SplicedColumnGroup(title = stringResource(R.string.http_cache)) {
|
||||
ClickableSettingItem(
|
||||
title = stringResource(R.string.cover_cache),
|
||||
description = stringResource(
|
||||
R.string.cache_size_mb,
|
||||
viewModel.coverCacheSize
|
||||
),
|
||||
onClick = { showClearCoverCacheDialog = true }
|
||||
)
|
||||
ClickableSettingItem(
|
||||
title = stringResource(R.string.manga_cache),
|
||||
description = stringResource(
|
||||
R.string.cache_size_mb,
|
||||
viewModel.mangaCacheSize
|
||||
),
|
||||
onClick = { showClearMangaCacheDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
SplicedColumnGroup(title = stringResource(R.string.download_setting)) {
|
||||
SliderSettingItem(
|
||||
title = stringResource(R.string.threads_num_title),
|
||||
description = stringResource(R.string.threads_num_summary),
|
||||
value = DownloadCacheConfig.threadCount.toFloat(),
|
||||
defaultValue = 8f,
|
||||
valueRange = 1f..256f,
|
||||
onValueChange = { DownloadCacheConfig.threadCount = it.toInt() }
|
||||
)
|
||||
|
||||
SliderSettingItem(
|
||||
title = stringResource(R.string.cache_book_threads_num_title),
|
||||
description = stringResource(R.string.cache_book_threads_num_summary),
|
||||
value = DownloadCacheConfig.cacheBookThreadCount
|
||||
.coerceIn(1, CacheBook.maxDownloadConcurrency)
|
||||
.toFloat(),
|
||||
defaultValue = CacheBook.maxDownloadConcurrency.toFloat(),
|
||||
valueRange = 1f..CacheBook.maxDownloadConcurrency.toFloat(),
|
||||
onValueChange = {
|
||||
DownloadCacheConfig.cacheBookThreadCount =
|
||||
it.toInt().coerceIn(1, CacheBook.maxDownloadConcurrency)
|
||||
}
|
||||
)
|
||||
|
||||
SliderSettingItem(
|
||||
title = stringResource(R.string.pre_download),
|
||||
description = stringResource(
|
||||
R.string.pre_download_s,
|
||||
DownloadCacheConfig.preDownloadNum
|
||||
),
|
||||
value = DownloadCacheConfig.preDownloadNum.toFloat(),
|
||||
defaultValue = 10f,
|
||||
valueRange = 0f..100f,
|
||||
onValueChange = { DownloadCacheConfig.preDownloadNum = it.toInt() }
|
||||
)
|
||||
}
|
||||
|
||||
SplicedColumnGroup(title = stringResource(R.string.image_cache)) {
|
||||
SliderSettingItem(
|
||||
title = stringResource(R.string.bitmap_cache_size),
|
||||
description = stringResource(
|
||||
R.string.bitmap_cache_size_summary,
|
||||
DownloadCacheConfig.bitmapCacheSize
|
||||
),
|
||||
value = DownloadCacheConfig.bitmapCacheSize.toFloat(),
|
||||
defaultValue = 32f,
|
||||
valueRange = 1f..2047f,
|
||||
onValueChange = {
|
||||
viewModel.updateBitmapCacheSize(it.toInt())
|
||||
}
|
||||
)
|
||||
|
||||
SliderSettingItem(
|
||||
title = stringResource(R.string.image_retain_number),
|
||||
description = stringResource(
|
||||
R.string.image_retain_number_summary,
|
||||
DownloadCacheConfig.imageRetainNum
|
||||
),
|
||||
value = DownloadCacheConfig.imageRetainNum.toFloat(),
|
||||
defaultValue = 10f,
|
||||
valueRange = 0f..100f,
|
||||
onValueChange = { DownloadCacheConfig.imageRetainNum = it.toInt() }
|
||||
)
|
||||
}
|
||||
|
||||
SplicedColumnGroup(title = stringResource(R.string.network)) {
|
||||
InputSettingItem(
|
||||
title = stringResource(R.string.user_agent),
|
||||
value = DownloadCacheConfig.userAgent,
|
||||
onConfirm = { viewModel.saveUserAgent(it) }
|
||||
)
|
||||
|
||||
SwitchSettingItem(
|
||||
title = "Cronet",
|
||||
description = stringResource(R.string.pref_cronet_summary),
|
||||
checked = DownloadCacheConfig.cronetEnable,
|
||||
onCheckedChange = { DownloadCacheConfig.cronetEnable = it }
|
||||
)
|
||||
}
|
||||
|
||||
SplicedColumnGroup(title = stringResource(R.string.other_setting)) {
|
||||
ClickableSettingItem(
|
||||
title = stringResource(R.string.clear_cache),
|
||||
description = stringResource(R.string.clear_cache_summary),
|
||||
onClick = { showClearBookCacheDialog = true }
|
||||
)
|
||||
|
||||
ClickableSettingItem(
|
||||
title = stringResource(R.string.shrink_database),
|
||||
description = stringResource(R.string.shrink_database_summary),
|
||||
onClick = { showShrinkDbDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppAlertDialog(
|
||||
show = showClearBookCacheDialog,
|
||||
onDismissRequest = { showClearBookCacheDialog = false },
|
||||
title = stringResource(R.string.clear_cache),
|
||||
text = stringResource(R.string.sure_del),
|
||||
onConfirm = {
|
||||
viewModel.clearBookCache(context)
|
||||
showClearBookCacheDialog = false
|
||||
},
|
||||
onDismiss = { showClearBookCacheDialog = false }
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showClearCoverCacheDialog,
|
||||
onDismissRequest = { showClearCoverCacheDialog = false },
|
||||
title = stringResource(R.string.cover_cache),
|
||||
text = stringResource(R.string.sure_del),
|
||||
onConfirm = {
|
||||
viewModel.clearCoverCache()
|
||||
showClearCoverCacheDialog = false
|
||||
},
|
||||
onDismiss = { showClearCoverCacheDialog = false }
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showClearMangaCacheDialog,
|
||||
onDismissRequest = { showClearMangaCacheDialog = false },
|
||||
title = stringResource(R.string.manga_cache),
|
||||
text = stringResource(R.string.sure_del),
|
||||
onConfirm = {
|
||||
viewModel.clearMangaCache()
|
||||
showClearMangaCacheDialog = false
|
||||
},
|
||||
onDismiss = { showClearMangaCacheDialog = false }
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showShrinkDbDialog,
|
||||
onDismissRequest = { showShrinkDbDialog = false },
|
||||
title = stringResource(R.string.shrink_database),
|
||||
text = stringResource(R.string.sure),
|
||||
onConfirm = {
|
||||
viewModel.shrinkDatabase()
|
||||
showShrinkDbDialog = false
|
||||
},
|
||||
onDismiss = { showShrinkDbDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package io.legado.app.ui.config.downloadCacheConfig
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableDoubleStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||
import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.HttpCacheType
|
||||
import io.legado.app.help.http.clearHttpCache
|
||||
import io.legado.app.help.http.getHttpCacheSize
|
||||
import io.legado.app.model.ImageProvider
|
||||
import io.legado.app.utils.FileUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DownloadCacheConfigViewModel(
|
||||
private val clearBookCacheUseCase: ClearBookCacheUseCase,
|
||||
private val shrinkDatabaseUseCase: ShrinkDatabaseUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
var coverCacheSize by mutableDoubleStateOf(0.0)
|
||||
var mangaCacheSize by mutableDoubleStateOf(0.0)
|
||||
|
||||
init {
|
||||
loadCacheSizes()
|
||||
}
|
||||
|
||||
fun loadCacheSizes() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
coverCacheSize = getHttpCacheSize(HttpCacheType.COVER) / (1024.0 * 1024.0)
|
||||
mangaCacheSize = getHttpCacheSize(HttpCacheType.MANGA) / (1024.0 * 1024.0)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCoverCache() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
clearHttpCache(HttpCacheType.COVER)
|
||||
coverCacheSize = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMangaCache() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
clearHttpCache(HttpCacheType.MANGA)
|
||||
mangaCacheSize = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
fun clearBookCache(context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
clearBookCacheUseCase.executeAll()
|
||||
FileUtils.delete(context.cacheDir.absolutePath)
|
||||
context.externalCacheDir?.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
fun shrinkDatabase() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
shrinkDatabaseUseCase.execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBitmapCacheSize(size: Int) {
|
||||
AppConfig.bitmapCacheSize = size
|
||||
ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize)
|
||||
}
|
||||
|
||||
fun saveUserAgent(input: String) {
|
||||
DownloadCacheConfig.userAgent = input
|
||||
AppConfig.userAgent = DownloadCacheConfig.userAgent
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user