diff --git a/app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt b/app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt index bf187c3f9..071e156cb 100644 --- a/app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt +++ b/app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt @@ -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() + + 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() 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 { 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) } } } diff --git a/app/src/main/java/io/legado/app/help/coil/CoverInterceptor.kt b/app/src/main/java/io/legado/app/help/coil/CoverInterceptor.kt index 2c0f8bf98..82b537355 100644 --- a/app/src/main/java/io/legado/app/help/coil/CoverInterceptor.kt +++ b/app/src/main/java/io/legado/app/help/coil/CoverInterceptor.kt @@ -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>>( + 16, 0.75f, true + ) { + override fun removeEldestEntry( + eldest: MutableMap.MutableEntry>>? + ): 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) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt index b1819b88a..f213f0db3 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt @@ -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") diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigNavScreen.kt b/app/src/main/java/io/legado/app/ui/config/ConfigNavScreen.kt index 1e6ab5f48..288180872 100644 --- a/app/src/main/java/io/legado/app/ui/config/ConfigNavScreen.kt +++ b/app/src/main/java/io/legado/app/ui/config/ConfigNavScreen.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigTag.kt b/app/src/main/java/io/legado/app/ui/config/ConfigTag.kt index 11b1c03a6..3bdd4f3b8 100644 --- a/app/src/main/java/io/legado/app/ui/config/ConfigTag.kt +++ b/app/src/main/java/io/legado/app/ui/config/ConfigTag.kt @@ -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" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfig.kt b/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfig.kt new file mode 100644 index 000000000..ed05c7a07 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfig.kt @@ -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" + +} diff --git a/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigScreen.kt new file mode 100644 index 000000000..28aa0bc06 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigScreen.kt @@ -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 } + ) + } +} diff --git a/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigViewModel.kt new file mode 100644 index 000000000..4b50c5403 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigViewModel.kt @@ -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 + } +}