mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Add chapter cache
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.jui.core.lang
|
||||
|
||||
fun Long.bytesIntoHumanReadable(si: Boolean = true): String {
|
||||
val bytes = this
|
||||
val unit = if (si) 1000L else 1024
|
||||
val i = if (si) "" else "i"
|
||||
val kilobyte: Long = unit
|
||||
val megabyte = kilobyte * unit
|
||||
val gigabyte = megabyte * unit
|
||||
val terabyte = gigabyte * unit
|
||||
return if (bytes in 0 until kilobyte) {
|
||||
"$bytes B"
|
||||
} else if (bytes in kilobyte until megabyte) {
|
||||
"${(bytes / kilobyte)} K${i}B"
|
||||
} else if (bytes in megabyte until gigabyte) {
|
||||
"${(bytes / megabyte)} M${i}B"
|
||||
} else if (bytes in gigabyte until terabyte) {
|
||||
"${(bytes / gigabyte)} G${i}B"
|
||||
} else if (bytes >= terabyte) {
|
||||
"${(bytes / terabyte)} T${i}B"
|
||||
} else {
|
||||
"$bytes Bytes"
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,13 @@ coroutines = "1.6.4"
|
||||
json = "1.4.0"
|
||||
|
||||
# Compose
|
||||
composeGradle = "1.2.0-alpha01-dev764"
|
||||
composeGradle = "1.2.0-alpha01-dev770"
|
||||
composeCompiler = "1.3.0"
|
||||
composeAndroid = "1.2.1"
|
||||
voyager = "1.0.0-beta16"
|
||||
accompanist = "0.25.1"
|
||||
googleAccompanist = "0.25.1"
|
||||
imageloader = "1.1.4"
|
||||
imageloader = "1.1.7"
|
||||
materialDialogs = "0.8.0"
|
||||
|
||||
# Android
|
||||
|
||||
@@ -296,6 +296,9 @@
|
||||
|
||||
<!-- Advanced Settings -->
|
||||
<string name="update_checker">Check for updates</string>
|
||||
<string name="clear_image_cache">Clear image cache</string>
|
||||
<string name="clear_chapter_cache">Clear chapter cache</string>
|
||||
<string name="clear_cache_sub">Used: %1$s</string>
|
||||
|
||||
<!-- Android notifications -->
|
||||
<string name="group_downloader">Downloader</string>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.jui.ui.base.image
|
||||
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import com.seiko.imageloader.component.decoder.BitmapFactoryDecoder
|
||||
import com.seiko.imageloader.component.decoder.Decoder
|
||||
|
||||
actual class BitmapDecoderFactory actual constructor(contextWrapper: ContextWrapper)
|
||||
: Decoder.Factory by BitmapFactoryDecoder.Factory(contextWrapper)
|
||||
@@ -18,9 +18,9 @@ actual fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilde
|
||||
return ImageLoaderBuilder(contextWrapper)
|
||||
}
|
||||
|
||||
actual fun diskCache(contextWrapper: ContextWrapper): DiskCache {
|
||||
actual fun diskCache(contextWrapper: ContextWrapper, cacheDir: String): DiskCache {
|
||||
return DiskCacheBuilder()
|
||||
.directory(contextWrapper.cacheDir.toOkioPath() / "image_cache")
|
||||
.directory(contextWrapper.cacheDir.toOkioPath() / cacheDir)
|
||||
.maxSizeBytes(1024 * 1024 * 150) // 150 MB
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -11,11 +11,17 @@ import androidx.compose.runtime.compositionLocalOf
|
||||
import ca.gosyer.jui.core.di.AppScope
|
||||
import ca.gosyer.jui.ui.ViewModelComponent
|
||||
import ca.gosyer.jui.ui.base.image.ImageLoaderProvider
|
||||
import ca.gosyer.jui.ui.base.image.diskCache
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import com.seiko.imageloader.ImageLoader
|
||||
import com.seiko.imageloader.LocalImageLoader
|
||||
import com.seiko.imageloader.cache.disk.DiskCache
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
|
||||
typealias ImageCache = DiskCache
|
||||
|
||||
typealias ChapterCache = DiskCache
|
||||
|
||||
interface UiComponent {
|
||||
val imageLoader: ImageLoader
|
||||
|
||||
@@ -23,9 +29,21 @@ interface UiComponent {
|
||||
|
||||
val hooks: Array<ProvidedValue<out Any>>
|
||||
|
||||
val imageCache: ImageCache
|
||||
|
||||
val chapterCache: ChapterCache
|
||||
|
||||
@AppScope
|
||||
@Provides
|
||||
fun imageLoaderFactory(imageLoaderProvider: ImageLoaderProvider): ImageLoader = imageLoaderProvider.get()
|
||||
fun imageLoaderFactory(imageLoaderProvider: ImageLoaderProvider, imageCache: ImageCache): ImageLoader = imageLoaderProvider.get(imageCache)
|
||||
|
||||
@AppScope
|
||||
@Provides
|
||||
fun imageCacheFactory(): ImageCache = diskCache(contextWrapper, "image_cache")
|
||||
|
||||
@AppScope
|
||||
@Provides
|
||||
fun chapterCacheFactory(): ChapterCache = diskCache(contextWrapper, "chapter_cache")
|
||||
|
||||
@Provides
|
||||
fun getHooks(viewModelComponent: ViewModelComponent) = arrayOf(
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.jui.ui.base.image
|
||||
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import com.seiko.imageloader.component.decoder.Decoder
|
||||
|
||||
expect class BitmapDecoderFactory constructor(contextWrapper: ContextWrapper) : Decoder.Factory
|
||||
@@ -11,6 +11,7 @@ import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.server.Http
|
||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.ui.base.ImageCache
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import com.seiko.imageloader.ImageLoader
|
||||
import com.seiko.imageloader.ImageLoaderBuilder
|
||||
@@ -31,7 +32,7 @@ class ImageLoaderProvider @Inject constructor(
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
|
||||
|
||||
fun get(): ImageLoader {
|
||||
fun get(imageCache: ImageCache): ImageLoader {
|
||||
return imageLoaderBuilder(context).apply {
|
||||
httpClient { http }
|
||||
components {
|
||||
@@ -48,7 +49,7 @@ class ImageLoaderProvider @Inject constructor(
|
||||
)
|
||||
)
|
||||
diskCache {
|
||||
diskCache(context)
|
||||
imageCache
|
||||
}
|
||||
memoryCache {
|
||||
memoryCache(context)
|
||||
@@ -104,6 +105,6 @@ class ImageLoaderProvider @Inject constructor(
|
||||
|
||||
expect fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilder
|
||||
|
||||
expect fun diskCache(contextWrapper: ContextWrapper): DiskCache
|
||||
expect fun diskCache(contextWrapper: ContextWrapper, cacheDir: String): DiskCache
|
||||
|
||||
expect fun memoryCache(contextWrapper: ContextWrapper): MemoryCache
|
||||
|
||||
@@ -8,9 +8,11 @@ package ca.gosyer.jui.ui.reader
|
||||
|
||||
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import ca.gosyer.jui.ui.base.image.BitmapDecoderFactory
|
||||
import ca.gosyer.jui.ui.reader.loader.PagesState
|
||||
import ca.gosyer.jui.ui.reader.loader.TachideskPageLoader
|
||||
import ca.gosyer.jui.ui.reader.model.ReaderChapter
|
||||
import com.seiko.imageloader.cache.disk.DiskCache
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.dropWhile
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
@@ -21,7 +23,9 @@ import org.lighthousegames.logging.logging
|
||||
|
||||
class ChapterLoader(
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val getChapterPage: GetChapterPage
|
||||
private val getChapterPage: GetChapterPage,
|
||||
private val chapterCache: DiskCache,
|
||||
private val bitmapDecoderFactory: BitmapDecoderFactory
|
||||
) {
|
||||
fun loadChapter(chapter: ReaderChapter): StateFlow<PagesState> {
|
||||
if (chapterIsReady(chapter)) {
|
||||
@@ -30,7 +34,7 @@ class ChapterLoader(
|
||||
chapter.state = ReaderChapter.State.Loading
|
||||
log.debug { "Loading pages for ${chapter.chapter.name}" }
|
||||
|
||||
val loader = TachideskPageLoader(chapter, readerPreferences, getChapterPage)
|
||||
val loader = TachideskPageLoader(chapter, readerPreferences, getChapterPage, chapterCache, bitmapDecoderFactory)
|
||||
|
||||
val pages = loader.getPages()
|
||||
|
||||
|
||||
@@ -40,17 +40,18 @@ import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.core.lang.withIOContext
|
||||
import ca.gosyer.jui.domain.reader.model.Direction
|
||||
import ca.gosyer.jui.domain.reader.model.ImageScale
|
||||
import ca.gosyer.jui.domain.reader.model.NavigationMode
|
||||
@@ -513,7 +514,7 @@ fun SideMenuButton(sideMenuOpen: Boolean, onOpenSideMenuClicked: () -> Unit) {
|
||||
@Composable
|
||||
fun ReaderImage(
|
||||
imageIndex: Int,
|
||||
drawableHolder: StableHolder<ImageBitmap?>,
|
||||
drawableHolder: StableHolder<(suspend () -> ReaderPage.ImageDecodeState)?>,
|
||||
progress: Float,
|
||||
status: ReaderPage.Status,
|
||||
error: String?,
|
||||
@@ -523,17 +524,35 @@ fun ReaderImage(
|
||||
retry: (Int) -> Unit
|
||||
) {
|
||||
Crossfade(drawableHolder to status) { (drawableHolder, status) ->
|
||||
val drawable = drawableHolder.item
|
||||
if (drawable != null) {
|
||||
val drawableCallback = drawableHolder.item
|
||||
val decodeState = produceState<ReaderPage.ImageDecodeState?>(null, drawableCallback) {
|
||||
if (drawableCallback != null) {
|
||||
withIOContext {
|
||||
value = drawableCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
val decode = decodeState.value
|
||||
if (decode != null && decode is ReaderPage.ImageDecodeState.Success) {
|
||||
Image(
|
||||
bitmap = drawable,
|
||||
bitmap = decode.bitmap,
|
||||
modifier = imageModifier,
|
||||
contentDescription = null,
|
||||
contentScale = contentScale,
|
||||
filterQuality = FilterQuality.High
|
||||
)
|
||||
} else {
|
||||
LoadingScreen(status == ReaderPage.Status.QUEUE, loadingModifier, progress, error) { retry(imageIndex) }
|
||||
LoadingScreen(
|
||||
status == ReaderPage.Status.QUEUE,
|
||||
loadingModifier,
|
||||
progress,
|
||||
error ?: when (decode) {
|
||||
is ReaderPage.ImageDecodeState.FailedToDecode -> decode.exception.message
|
||||
ReaderPage.ImageDecodeState.UnknownDecoder -> "Unknown decoder"
|
||||
ReaderPage.ImageDecodeState.FailedToGetSnapShot -> "Failed to get snapshot"
|
||||
else -> null
|
||||
}
|
||||
) { retry(imageIndex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import ca.gosyer.jui.domain.manga.model.MangaMeta
|
||||
import ca.gosyer.jui.domain.reader.ReaderModeWatch
|
||||
import ca.gosyer.jui.domain.reader.model.Direction
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import ca.gosyer.jui.ui.base.ChapterCache
|
||||
import ca.gosyer.jui.ui.base.image.BitmapDecoderFactory
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.reader.loader.PagesState
|
||||
import ca.gosyer.jui.ui.reader.model.MoveTo
|
||||
@@ -67,6 +69,7 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
private val updateChapterFlags: UpdateChapterFlags,
|
||||
private val updateMangaMeta: UpdateMangaMeta,
|
||||
private val updateChapterMeta: UpdateChapterMeta,
|
||||
private val chapterCache: ChapterCache,
|
||||
contextWrapper: ContextWrapper,
|
||||
private val params: Params
|
||||
) : ViewModel(contextWrapper) {
|
||||
@@ -116,7 +119,12 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
|
||||
val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode)
|
||||
|
||||
private val loader = ChapterLoader(readerPreferences, getChapterPage)
|
||||
private val loader = ChapterLoader(
|
||||
readerPreferences = readerPreferences,
|
||||
getChapterPage = getChapterPage,
|
||||
chapterCache = chapterCache,
|
||||
bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper)
|
||||
)
|
||||
|
||||
init {
|
||||
init()
|
||||
|
||||
@@ -6,16 +6,24 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.loader
|
||||
|
||||
import androidx.compose.ui.graphics.asComposeImageBitmap
|
||||
import ca.gosyer.jui.core.lang.throwIfCancellation
|
||||
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import ca.gosyer.jui.ui.base.image.BitmapDecoderFactory
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.jui.ui.reader.model.ReaderPage
|
||||
import ca.gosyer.jui.ui.util.compose.toImageBitmap
|
||||
import ca.gosyer.jui.ui.util.lang.priorityChannel
|
||||
import cafe.adriel.voyager.core.concurrent.AtomicInt32
|
||||
import com.seiko.imageloader.cache.disk.DiskCache
|
||||
import com.seiko.imageloader.component.decoder.DecodeImageResult
|
||||
import com.seiko.imageloader.request.ImageRequestBuilder
|
||||
import com.seiko.imageloader.request.Options
|
||||
import com.seiko.imageloader.request.SourceResult
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -28,12 +36,18 @@ import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.BufferedSource
|
||||
import okio.FileSystem
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class TachideskPageLoader(
|
||||
val chapter: ReaderChapter,
|
||||
readerPreferences: ReaderPreferences,
|
||||
getChapterPage: GetChapterPage
|
||||
getChapterPage: GetChapterPage,
|
||||
private val chapterCache: DiskCache,
|
||||
private val bitmapDecoderFactory: BitmapDecoderFactory
|
||||
) : PageLoader() {
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@@ -69,7 +83,39 @@ class TachideskPageLoader(
|
||||
}
|
||||
}
|
||||
.onEach {
|
||||
page.bitmap.value = StableHolder(it.toImageBitmap())
|
||||
val editor = chapterCache.edit(page.cacheKey)
|
||||
?: throw Exception("Couldn't open cache")
|
||||
try {
|
||||
FileSystem.SYSTEM.write(editor.data) {
|
||||
it.bodyAsChannel().toInputStream().source().use {
|
||||
writeAll(it)
|
||||
}
|
||||
}
|
||||
editor.commit()
|
||||
} catch (e: Exception) {
|
||||
editor.abortQuietly()
|
||||
throw e
|
||||
}
|
||||
page.bitmap.value = StableHolder {
|
||||
chapterCache[page.cacheKey]?.use {
|
||||
val decoder = bitmapDecoderFactory.create(
|
||||
SourceResult(
|
||||
ImageRequestBuilder().build(),
|
||||
it.source()
|
||||
),
|
||||
Options()
|
||||
)
|
||||
if (decoder != null) {
|
||||
runCatching { decoder.decode() as DecodeImageResult }
|
||||
.mapCatching {
|
||||
ReaderPage.ImageDecodeState.Success(it.image.asComposeImageBitmap())
|
||||
}
|
||||
.getOrElse {
|
||||
ReaderPage.ImageDecodeState.FailedToDecode(it)
|
||||
}
|
||||
} else ReaderPage.ImageDecodeState.UnknownDecoder
|
||||
} ?: ReaderPage.ImageDecodeState.FailedToGetSnapShot
|
||||
}
|
||||
page.status.value = ReaderPage.Status.READY
|
||||
page.error.value = null
|
||||
}
|
||||
@@ -195,4 +241,17 @@ class TachideskPageLoader(
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
|
||||
private val ReaderPage.cacheKey
|
||||
get() = "${chapter.chapter.mangaId}-${chapter.chapter.index}-${index}"
|
||||
|
||||
private fun DiskCache.Snapshot.source(): BufferedSource {
|
||||
return FileSystem.SYSTEM.source(data).buffer()
|
||||
}
|
||||
|
||||
private fun DiskCache.Editor.abortQuietly() {
|
||||
try {
|
||||
abort()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Immutable
|
||||
data class ReaderPage(
|
||||
val index: Int,
|
||||
val bitmap: MutableStateFlow<StableHolder<ImageBitmap?>>,
|
||||
val bitmap: MutableStateFlow<StableHolder<(suspend () -> ImageDecodeState)?>>,
|
||||
val progress: MutableStateFlow<Float>,
|
||||
val status: MutableStateFlow<Status>,
|
||||
val error: MutableStateFlow<String?>,
|
||||
@@ -25,4 +25,16 @@ data class ReaderPage(
|
||||
READY,
|
||||
ERROR
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class ImageDecodeState {
|
||||
@Immutable
|
||||
data class Success(val bitmap: ImageBitmap) : ImageDecodeState()
|
||||
@Immutable
|
||||
object UnknownDecoder : ImageDecodeState()
|
||||
@Immutable
|
||||
object FailedToGetSnapShot : ImageDecodeState()
|
||||
@Immutable
|
||||
data class FailedToDecode(val exception: Throwable) : ImageDecodeState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,20 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import ca.gosyer.jui.core.lang.bytesIntoHumanReadable
|
||||
import ca.gosyer.jui.core.lang.launchIO
|
||||
import ca.gosyer.jui.domain.updates.service.UpdatePreferences
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.ChapterCache
|
||||
import ca.gosyer.jui.ui.base.ImageCache
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.jui.ui.base.prefs.SwitchPreference
|
||||
import ca.gosyer.jui.ui.main.components.bottomNav
|
||||
import ca.gosyer.jui.ui.viewModel
|
||||
@@ -40,7 +47,15 @@ import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.isActive
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class SettingsAdvancedScreen : Screen {
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
@@ -49,21 +64,60 @@ class SettingsAdvancedScreen : Screen {
|
||||
override fun Content() {
|
||||
val vm = viewModel { settingsAdvancedViewModel() }
|
||||
SettingsAdvancedScreenContent(
|
||||
updatesEnabled = vm.updatesEnabled
|
||||
updatesEnabled = vm.updatesEnabled,
|
||||
imageCacheSize = vm.imageCacheSize.collectAsState().value,
|
||||
clearImageCache = vm::clearImageCache,
|
||||
chapterCacheSize = vm.chapterCacheSize.collectAsState().value,
|
||||
clearChapterCache = vm::clearChapterCache
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsAdvancedViewModel @Inject constructor(
|
||||
updatePreferences: UpdatePreferences,
|
||||
private val imageCache: ImageCache,
|
||||
private val chapterCache: ChapterCache,
|
||||
contextWrapper: ContextWrapper
|
||||
) : ViewModel(contextWrapper) {
|
||||
val updatesEnabled = updatePreferences.enabled().asStateFlow()
|
||||
|
||||
val imageCacheSize = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
emit(imageCache.size.bytesIntoHumanReadable())
|
||||
delay(1.seconds)
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
val chapterCacheSize = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
emit(chapterCache.size.bytesIntoHumanReadable())
|
||||
delay(1.seconds)
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
fun clearImageCache() {
|
||||
scope.launchIO {
|
||||
imageCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChapterCache() {
|
||||
scope.launchIO {
|
||||
chapterCache.clear()
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsAdvancedScreenContent(
|
||||
updatesEnabled: PreferenceMutableStateFlow<Boolean>
|
||||
updatesEnabled: PreferenceMutableStateFlow<Boolean>,
|
||||
imageCacheSize: String,
|
||||
clearImageCache: () -> Unit,
|
||||
chapterCacheSize: String,
|
||||
clearChapterCache: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
@@ -89,6 +143,23 @@ fun SettingsAdvancedScreenContent(
|
||||
item {
|
||||
SwitchPreference(preference = updatesEnabled, title = stringResource(MR.strings.update_checker))
|
||||
}
|
||||
item {
|
||||
Divider()
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.clear_image_cache),
|
||||
subtitle = stringResource(MR.strings.clear_cache_sub, imageCacheSize),
|
||||
onClick = clearImageCache
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.clear_chapter_cache),
|
||||
subtitle = stringResource(MR.strings.clear_cache_sub, chapterCacheSize),
|
||||
onClick = clearChapterCache
|
||||
)
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
rememberScrollbarAdapter(state),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.jui.ui.base.image
|
||||
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import com.seiko.imageloader.component.decoder.Decoder
|
||||
import com.seiko.imageloader.component.decoder.SkiaImageDecoder
|
||||
|
||||
actual class BitmapDecoderFactory actual constructor(contextWrapper: ContextWrapper)
|
||||
: Decoder.Factory by SkiaImageDecoder.Factory()
|
||||
@@ -18,9 +18,9 @@ actual fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilde
|
||||
return ImageLoaderBuilder()
|
||||
}
|
||||
|
||||
actual fun diskCache(contextWrapper: ContextWrapper): DiskCache {
|
||||
actual fun diskCache(contextWrapper: ContextWrapper, cacheDir: String): DiskCache {
|
||||
return DiskCacheBuilder()
|
||||
.directory(userDataDir / "image_cache")
|
||||
.directory(userDataDir / cacheDir)
|
||||
.maxSizeBytes(1024 * 1024 * 150) // 150 MB
|
||||
.build()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user