Add chapter cache

This commit is contained in:
Syer10
2022-08-27 12:23:24 -04:00
parent 8cb8a86052
commit 3c0313b7fb
16 changed files with 290 additions and 25 deletions

View File

@@ -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"
}
}

View File

@@ -7,13 +7,13 @@ coroutines = "1.6.4"
json = "1.4.0" json = "1.4.0"
# Compose # Compose
composeGradle = "1.2.0-alpha01-dev764" composeGradle = "1.2.0-alpha01-dev770"
composeCompiler = "1.3.0" composeCompiler = "1.3.0"
composeAndroid = "1.2.1" composeAndroid = "1.2.1"
voyager = "1.0.0-beta16" voyager = "1.0.0-beta16"
accompanist = "0.25.1" accompanist = "0.25.1"
googleAccompanist = "0.25.1" googleAccompanist = "0.25.1"
imageloader = "1.1.4" imageloader = "1.1.7"
materialDialogs = "0.8.0" materialDialogs = "0.8.0"
# Android # Android

View File

@@ -296,6 +296,9 @@
<!-- Advanced Settings --> <!-- Advanced Settings -->
<string name="update_checker">Check for updates</string> <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 --> <!-- Android notifications -->
<string name="group_downloader">Downloader</string> <string name="group_downloader">Downloader</string>

View File

@@ -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)

View File

@@ -18,9 +18,9 @@ actual fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilde
return ImageLoaderBuilder(contextWrapper) return ImageLoaderBuilder(contextWrapper)
} }
actual fun diskCache(contextWrapper: ContextWrapper): DiskCache { actual fun diskCache(contextWrapper: ContextWrapper, cacheDir: String): DiskCache {
return DiskCacheBuilder() return DiskCacheBuilder()
.directory(contextWrapper.cacheDir.toOkioPath() / "image_cache") .directory(contextWrapper.cacheDir.toOkioPath() / cacheDir)
.maxSizeBytes(1024 * 1024 * 150) // 150 MB .maxSizeBytes(1024 * 1024 * 150) // 150 MB
.build() .build()
} }

View File

@@ -11,11 +11,17 @@ import androidx.compose.runtime.compositionLocalOf
import ca.gosyer.jui.core.di.AppScope import ca.gosyer.jui.core.di.AppScope
import ca.gosyer.jui.ui.ViewModelComponent import ca.gosyer.jui.ui.ViewModelComponent
import ca.gosyer.jui.ui.base.image.ImageLoaderProvider import ca.gosyer.jui.ui.base.image.ImageLoaderProvider
import ca.gosyer.jui.ui.base.image.diskCache
import ca.gosyer.jui.uicore.vm.ContextWrapper import ca.gosyer.jui.uicore.vm.ContextWrapper
import com.seiko.imageloader.ImageLoader import com.seiko.imageloader.ImageLoader
import com.seiko.imageloader.LocalImageLoader import com.seiko.imageloader.LocalImageLoader
import com.seiko.imageloader.cache.disk.DiskCache
import me.tatarka.inject.annotations.Provides import me.tatarka.inject.annotations.Provides
typealias ImageCache = DiskCache
typealias ChapterCache = DiskCache
interface UiComponent { interface UiComponent {
val imageLoader: ImageLoader val imageLoader: ImageLoader
@@ -23,9 +29,21 @@ interface UiComponent {
val hooks: Array<ProvidedValue<out Any>> val hooks: Array<ProvidedValue<out Any>>
val imageCache: ImageCache
val chapterCache: ChapterCache
@AppScope @AppScope
@Provides @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 @Provides
fun getHooks(viewModelComponent: ViewModelComponent) = arrayOf( fun getHooks(viewModelComponent: ViewModelComponent) = arrayOf(

View File

@@ -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

View File

@@ -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.Http
import ca.gosyer.jui.domain.server.service.ServerPreferences import ca.gosyer.jui.domain.server.service.ServerPreferences
import ca.gosyer.jui.domain.source.model.Source import ca.gosyer.jui.domain.source.model.Source
import ca.gosyer.jui.ui.base.ImageCache
import ca.gosyer.jui.uicore.vm.ContextWrapper import ca.gosyer.jui.uicore.vm.ContextWrapper
import com.seiko.imageloader.ImageLoader import com.seiko.imageloader.ImageLoader
import com.seiko.imageloader.ImageLoaderBuilder import com.seiko.imageloader.ImageLoaderBuilder
@@ -31,7 +32,7 @@ class ImageLoaderProvider @Inject constructor(
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope) val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
fun get(): ImageLoader { fun get(imageCache: ImageCache): ImageLoader {
return imageLoaderBuilder(context).apply { return imageLoaderBuilder(context).apply {
httpClient { http } httpClient { http }
components { components {
@@ -48,7 +49,7 @@ class ImageLoaderProvider @Inject constructor(
) )
) )
diskCache { diskCache {
diskCache(context) imageCache
} }
memoryCache { memoryCache {
memoryCache(context) memoryCache(context)
@@ -104,6 +105,6 @@ class ImageLoaderProvider @Inject constructor(
expect fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilder 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 expect fun memoryCache(contextWrapper: ContextWrapper): MemoryCache

View File

@@ -8,9 +8,11 @@ package ca.gosyer.jui.ui.reader
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
import ca.gosyer.jui.domain.reader.service.ReaderPreferences 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.PagesState
import ca.gosyer.jui.ui.reader.loader.TachideskPageLoader import ca.gosyer.jui.ui.reader.loader.TachideskPageLoader
import ca.gosyer.jui.ui.reader.model.ReaderChapter import ca.gosyer.jui.ui.reader.model.ReaderChapter
import com.seiko.imageloader.cache.disk.DiskCache
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
@@ -21,7 +23,9 @@ import org.lighthousegames.logging.logging
class ChapterLoader( class ChapterLoader(
private val readerPreferences: ReaderPreferences, 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> { fun loadChapter(chapter: ReaderChapter): StateFlow<PagesState> {
if (chapterIsReady(chapter)) { if (chapterIsReady(chapter)) {
@@ -30,7 +34,7 @@ class ChapterLoader(
chapter.state = ReaderChapter.State.Loading chapter.state = ReaderChapter.State.Loading
log.debug { "Loading pages for ${chapter.chapter.name}" } 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() val pages = loader.getPages()

View File

@@ -40,17 +40,18 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality 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.Key
import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp 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.Direction
import ca.gosyer.jui.domain.reader.model.ImageScale import ca.gosyer.jui.domain.reader.model.ImageScale
import ca.gosyer.jui.domain.reader.model.NavigationMode import ca.gosyer.jui.domain.reader.model.NavigationMode
@@ -513,7 +514,7 @@ fun SideMenuButton(sideMenuOpen: Boolean, onOpenSideMenuClicked: () -> Unit) {
@Composable @Composable
fun ReaderImage( fun ReaderImage(
imageIndex: Int, imageIndex: Int,
drawableHolder: StableHolder<ImageBitmap?>, drawableHolder: StableHolder<(suspend () -> ReaderPage.ImageDecodeState)?>,
progress: Float, progress: Float,
status: ReaderPage.Status, status: ReaderPage.Status,
error: String?, error: String?,
@@ -523,17 +524,35 @@ fun ReaderImage(
retry: (Int) -> Unit retry: (Int) -> Unit
) { ) {
Crossfade(drawableHolder to status) { (drawableHolder, status) -> Crossfade(drawableHolder to status) { (drawableHolder, status) ->
val drawable = drawableHolder.item val drawableCallback = drawableHolder.item
if (drawable != null) { 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( Image(
bitmap = drawable, bitmap = decode.bitmap,
modifier = imageModifier, modifier = imageModifier,
contentDescription = null, contentDescription = null,
contentScale = contentScale, contentScale = contentScale,
filterQuality = FilterQuality.High filterQuality = FilterQuality.High
) )
} else { } 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) }
} }
} }
} }

View File

@@ -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.ReaderModeWatch
import ca.gosyer.jui.domain.reader.model.Direction import ca.gosyer.jui.domain.reader.model.Direction
import ca.gosyer.jui.domain.reader.service.ReaderPreferences 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.base.model.StableHolder
import ca.gosyer.jui.ui.reader.loader.PagesState import ca.gosyer.jui.ui.reader.loader.PagesState
import ca.gosyer.jui.ui.reader.model.MoveTo import ca.gosyer.jui.ui.reader.model.MoveTo
@@ -67,6 +69,7 @@ class ReaderMenuViewModel @Inject constructor(
private val updateChapterFlags: UpdateChapterFlags, private val updateChapterFlags: UpdateChapterFlags,
private val updateMangaMeta: UpdateMangaMeta, private val updateMangaMeta: UpdateMangaMeta,
private val updateChapterMeta: UpdateChapterMeta, private val updateChapterMeta: UpdateChapterMeta,
private val chapterCache: ChapterCache,
contextWrapper: ContextWrapper, contextWrapper: ContextWrapper,
private val params: Params private val params: Params
) : ViewModel(contextWrapper) { ) : ViewModel(contextWrapper) {
@@ -116,7 +119,12 @@ class ReaderMenuViewModel @Inject constructor(
val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode) 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 {
init() init()

View File

@@ -6,16 +6,24 @@
package ca.gosyer.jui.ui.reader.loader package ca.gosyer.jui.ui.reader.loader
import androidx.compose.ui.graphics.asComposeImageBitmap
import ca.gosyer.jui.core.lang.throwIfCancellation import ca.gosyer.jui.core.lang.throwIfCancellation
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
import ca.gosyer.jui.domain.reader.service.ReaderPreferences 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.base.model.StableHolder
import ca.gosyer.jui.ui.reader.model.ReaderChapter import ca.gosyer.jui.ui.reader.model.ReaderChapter
import ca.gosyer.jui.ui.reader.model.ReaderPage 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 ca.gosyer.jui.ui.util.lang.priorityChannel
import cafe.adriel.voyager.core.concurrent.AtomicInt32 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.plugins.onDownload
import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -28,12 +36,18 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okio.BufferedSource
import okio.FileSystem
import okio.buffer
import okio.source
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class TachideskPageLoader( class TachideskPageLoader(
val chapter: ReaderChapter, val chapter: ReaderChapter,
readerPreferences: ReaderPreferences, readerPreferences: ReaderPreferences,
getChapterPage: GetChapterPage getChapterPage: GetChapterPage,
private val chapterCache: DiskCache,
private val bitmapDecoderFactory: BitmapDecoderFactory
) : PageLoader() { ) : PageLoader() {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -69,7 +83,39 @@ class TachideskPageLoader(
} }
} }
.onEach { .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.status.value = ReaderPage.Status.READY
page.error.value = null page.error.value = null
} }
@@ -195,4 +241,17 @@ class TachideskPageLoader(
private companion object { private companion object {
private val log = logging() 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) {}
}
} }

View File

@@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Immutable @Immutable
data class ReaderPage( data class ReaderPage(
val index: Int, val index: Int,
val bitmap: MutableStateFlow<StableHolder<ImageBitmap?>>, val bitmap: MutableStateFlow<StableHolder<(suspend () -> ImageDecodeState)?>>,
val progress: MutableStateFlow<Float>, val progress: MutableStateFlow<Float>,
val status: MutableStateFlow<Status>, val status: MutableStateFlow<Status>,
val error: MutableStateFlow<String?>, val error: MutableStateFlow<String?>,
@@ -25,4 +25,16 @@ data class ReaderPage(
READY, READY,
ERROR 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()
}
} }

View File

@@ -18,13 +18,20 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Divider
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.domain.updates.service.UpdatePreferences
import ca.gosyer.jui.i18n.MR 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.navigation.Toolbar
import ca.gosyer.jui.ui.base.prefs.PreferenceRow
import ca.gosyer.jui.ui.base.prefs.SwitchPreference import ca.gosyer.jui.ui.base.prefs.SwitchPreference
import ca.gosyer.jui.ui.main.components.bottomNav import ca.gosyer.jui.ui.main.components.bottomNav
import ca.gosyer.jui.ui.viewModel 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.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey 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 me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
import kotlin.time.Duration.Companion.seconds
class SettingsAdvancedScreen : Screen { class SettingsAdvancedScreen : Screen {
override val key: ScreenKey = uniqueScreenKey override val key: ScreenKey = uniqueScreenKey
@@ -49,21 +64,60 @@ class SettingsAdvancedScreen : Screen {
override fun Content() { override fun Content() {
val vm = viewModel { settingsAdvancedViewModel() } val vm = viewModel { settingsAdvancedViewModel() }
SettingsAdvancedScreenContent( 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( class SettingsAdvancedViewModel @Inject constructor(
updatePreferences: UpdatePreferences, updatePreferences: UpdatePreferences,
private val imageCache: ImageCache,
private val chapterCache: ChapterCache,
contextWrapper: ContextWrapper contextWrapper: ContextWrapper
) : ViewModel(contextWrapper) { ) : ViewModel(contextWrapper) {
val updatesEnabled = updatePreferences.enabled().asStateFlow() 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 @Composable
fun SettingsAdvancedScreenContent( fun SettingsAdvancedScreenContent(
updatesEnabled: PreferenceMutableStateFlow<Boolean> updatesEnabled: PreferenceMutableStateFlow<Boolean>,
imageCacheSize: String,
clearImageCache: () -> Unit,
chapterCacheSize: String,
clearChapterCache: () -> Unit,
) { ) {
Scaffold( Scaffold(
modifier = Modifier.windowInsetsPadding( modifier = Modifier.windowInsetsPadding(
@@ -89,6 +143,23 @@ fun SettingsAdvancedScreenContent(
item { item {
SwitchPreference(preference = updatesEnabled, title = stringResource(MR.strings.update_checker)) 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( VerticalScrollbar(
rememberScrollbarAdapter(state), rememberScrollbarAdapter(state),

View File

@@ -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()

View File

@@ -18,9 +18,9 @@ actual fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilde
return ImageLoaderBuilder() return ImageLoaderBuilder()
} }
actual fun diskCache(contextWrapper: ContextWrapper): DiskCache { actual fun diskCache(contextWrapper: ContextWrapper, cacheDir: String): DiskCache {
return DiskCacheBuilder() return DiskCacheBuilder()
.directory(userDataDir / "image_cache") .directory(userDataDir / cacheDir)
.maxSizeBytes(1024 * 1024 * 150) // 150 MB .maxSizeBytes(1024 * 1024 * 150) // 150 MB
.build() .build()
} }