mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 14:52:03 +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"
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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.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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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()
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user