diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/Humanize.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/Humanize.kt
new file mode 100644
index 00000000..93289e44
--- /dev/null
+++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/Humanize.kt
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5dd3052e..ceed83aa 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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
diff --git a/i18n/src/commonMain/resources/MR/values/base/strings.xml b/i18n/src/commonMain/resources/MR/values/base/strings.xml
index 9e49e364..fc434966 100644
--- a/i18n/src/commonMain/resources/MR/values/base/strings.xml
+++ b/i18n/src/commonMain/resources/MR/values/base/strings.xml
@@ -296,6 +296,9 @@
Check for updates
+ Clear image cache
+ Clear chapter cache
+ Used: %1$s
Downloader
diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidBitmapDecoderFactory.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidBitmapDecoderFactory.kt
new file mode 100644
index 00000000..db3d68b8
--- /dev/null
+++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidBitmapDecoderFactory.kt
@@ -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)
\ No newline at end of file
diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt
index faeb758f..1befcc4c 100644
--- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt
+++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt
@@ -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()
}
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt
index 044974a7..6186651a 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt
@@ -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>
+ 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(
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/BitmapDecoderFactory.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/BitmapDecoderFactory.kt
new file mode 100644
index 00000000..81649431
--- /dev/null
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/BitmapDecoderFactory.kt
@@ -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
\ No newline at end of file
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt
index 4d23668f..8fb958e9 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt
@@ -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
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt
index a46765e6..34a21bed 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ChapterLoader.kt
@@ -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 {
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()
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt
index 3a435d02..3f050765 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt
@@ -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,
+ 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(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) }
}
}
}
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt
index 7bec9c97..23c2902d 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt
@@ -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()
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt
index 5ab8a05f..92dee0d4 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt
@@ -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) {}
+ }
}
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/ReaderPage.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/ReaderPage.kt
index e2f922e7..a260f2ad 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/ReaderPage.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/ReaderPage.kt
@@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Immutable
data class ReaderPage(
val index: Int,
- val bitmap: MutableStateFlow>,
+ val bitmap: MutableStateFlow ImageDecodeState)?>>,
val progress: MutableStateFlow,
val status: MutableStateFlow,
val error: MutableStateFlow,
@@ -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()
+ }
}
diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt
index 7a4daf2c..61f91d37 100644
--- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt
+++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt
@@ -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
+ updatesEnabled: PreferenceMutableStateFlow,
+ 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),
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopBitmapDecoderFactory.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopBitmapDecoderFactory.kt
new file mode 100644
index 00000000..0f5b891d
--- /dev/null
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopBitmapDecoderFactory.kt
@@ -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()
\ No newline at end of file
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt
index 8e2717d0..7f0ecbec 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt
@@ -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()
}