diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 512506e5..7556ec11 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(libs.accompanist.pagerIndicators) implementation(libs.accompanist.flowLayout) implementation(libs.kamel) + implementation(libs.imageloader) implementation(libs.materialDialogs.core) // Android diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 033ef953..5f8c3fcf 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(libs.accompanist.pagerIndicators) implementation(libs.accompanist.flowLayout) implementation(libs.kamel) + implementation(libs.imageloader) implementation(libs.materialDialogs.core) // UI (Swing) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5eafe18..d9f0ccdc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ composeAndroid = "1.2.1" voyager = "1.0.0-beta16" accompanist = "0.25.1" kamel = "0.4.1" +imageloader = "1.1.3" materialDialogs = "0.8.0" # Android @@ -101,6 +102,7 @@ accompanist-pager = { module = "ca.gosyer:accompanist-pager", version.ref = "acc accompanist-pagerIndicators = { module = "ca.gosyer:accompanist-pager-indicators", version.ref = "accompanist" } accompanist-flowLayout = { module = "ca.gosyer:accompanist-flowlayout", version.ref = "accompanist" } kamel = { module = "com.alialbaali.kamel:kamel-image", version.ref = "kamel" } +imageloader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "imageloader" } materialDialogs-core = { module = "ca.gosyer:compose-material-dialogs-core", version.ref = "materialDialogs" } # Android diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 97eaaa98..313ffd14 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -43,6 +43,7 @@ kotlin { api(kotlin("stdlib-common")) api(libs.coroutines.core) api(libs.kamel) + api(libs.imageloader) api(libs.voyager.core) api(libs.voyager.navigation) api(libs.voyager.transitions) 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 new file mode 100644 index 00000000..70898761 --- /dev/null +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt @@ -0,0 +1,31 @@ +/* + * 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.ImageLoaderBuilder +import com.seiko.imageloader.cache.disk.DiskCache +import com.seiko.imageloader.cache.disk.DiskCacheBuilder +import com.seiko.imageloader.cache.memory.MemoryCache +import com.seiko.imageloader.cache.memory.MemoryCacheBuilder +import okio.Path.Companion.toOkioPath + +actual fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilder { + return ImageLoaderBuilder(contextWrapper) +} + +actual fun diskCache(contextWrapper: ContextWrapper): DiskCache { + return DiskCacheBuilder() + .directory(contextWrapper.cacheDir.toOkioPath() / "image_cache") + .maxSizeBytes(1024 * 1024 * 150) // 150 MB + .build() +} + +actual fun memoryCache(contextWrapper: ContextWrapper): MemoryCache { + return MemoryCacheBuilder(contextWrapper) + .build() +} \ No newline at end of file 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 db571eff..0d282c2b 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 @@ -10,8 +10,11 @@ import androidx.compose.runtime.ProvidedValue 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.KamelConfigProvider import ca.gosyer.jui.uicore.vm.ContextWrapper +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.LocalImageLoader import io.kamel.core.config.KamelConfig import io.kamel.core.config.KamelConfigBuilder import io.kamel.image.config.LocalKamelConfig @@ -22,6 +25,8 @@ interface UiComponent { val kamelConfig: KamelConfig + val imageLoader: ImageLoader + val contextWrapper: ContextWrapper val hooks: Array> @@ -30,10 +35,15 @@ interface UiComponent { @Provides fun kamelConfigFactory(contextWrapper: ContextWrapper): KamelConfig = kamelConfigProvider.get { kamelPlatformHandler(contextWrapper) } + @AppScope + @Provides + fun imageLoaderFactory(imageLoaderProvider: ImageLoaderProvider): ImageLoader = imageLoaderProvider.get() + @Provides fun getHooks(viewModelComponent: ViewModelComponent) = arrayOf( LocalViewModels provides viewModelComponent, - LocalKamelConfig provides kamelConfig + LocalKamelConfig provides kamelConfig, + LocalImageLoader provides imageLoader ) } 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 new file mode 100644 index 00000000..f85120d3 --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt @@ -0,0 +1,84 @@ +/* + * 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.domain.extension.model.Extension +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.uicore.vm.ContextWrapper +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.ImageLoaderBuilder +import com.seiko.imageloader.cache.disk.DiskCache +import com.seiko.imageloader.cache.memory.MemoryCache +import com.seiko.imageloader.component.mapper.Mapper +import com.seiko.imageloader.request.Options +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import me.tatarka.inject.annotations.Inject + +class ImageLoaderProvider @Inject constructor( + private val http: Http, + serverPreferences: ServerPreferences, + private val context: ContextWrapper +) { + @OptIn(DelicateCoroutinesApi::class) + val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope) + + fun get(): ImageLoader { + return imageLoaderBuilder(context).apply { + httpClient { http } + components { + add(MangaCoverMapper()) + add(ExtensionIconMapper()) + add(SourceIconMapper()) + } + options( + Options( + config = Options.ImageConfig.HARDWARE + ) + ) + diskCache { + diskCache(context) + } + memoryCache { + memoryCache(context) + } + }.build() + } + + inner class MangaCoverMapper : Mapper { + override fun map(data: Any, options: Options): String? { + if (data !is Manga) return null + if (data.thumbnailUrl.isNullOrBlank()) return null + return serverUrl.value.toString() + data.thumbnailUrl + } + } + + inner class ExtensionIconMapper : Mapper { + override fun map(data: Any, options: Options): String? { + if (data !is Extension) return null + if (data.iconUrl.isBlank()) return null + return serverUrl.value.toString() + data.iconUrl + } + } + + inner class SourceIconMapper : Mapper { + override fun map(data: Any, options: Options): String? { + if (data !is Source) return null + if (data.iconUrl.isBlank()) return null + return serverUrl.value.toString() + data.iconUrl + } + } +} + +expect fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilder + +expect fun diskCache(contextWrapper: ContextWrapper): DiskCache + +expect fun memoryCache(contextWrapper: ContextWrapper): MemoryCache \ No newline at end of file diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt index 02176843..7dabcff8 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import ca.gosyer.jui.domain.chapter.model.Chapter @@ -58,7 +57,6 @@ import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding import ca.gosyer.jui.uicore.resources.stringResource -import io.kamel.image.lazyPainterResource @Composable fun DownloadsScreenContent( @@ -128,7 +126,7 @@ fun DownloadsItem( .padding(start = 16.dp, top = 8.dp, bottom = 8.dp) .clip(MaterialTheme.shapes.medium) .clickable { onClickCover() }, - cover = lazyPainterResource(item.manga, filterQuality = FilterQuality.Medium), + data = item.manga, contentDescription = item.manga.title ) MangaListItemColumn( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt index b06851c0..6a6ae342 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt @@ -54,14 +54,13 @@ import ca.gosyer.jui.uicore.components.LoadingScreen import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage +import ca.gosyer.jui.uicore.image.ImageLoaderImage import ca.gosyer.jui.uicore.resources.stringResource import com.vanpra.composematerialdialogs.MaterialDialog import com.vanpra.composematerialdialogs.MaterialDialogState import com.vanpra.composematerialdialogs.listItemsMultiChoice import com.vanpra.composematerialdialogs.rememberMaterialDialogState import com.vanpra.composematerialdialogs.title -import io.kamel.image.lazyPainterResource @Composable fun ExtensionsScreenContent( @@ -153,7 +152,12 @@ fun ExtensionItem( Box(modifier = Modifier.fillMaxWidth().padding(end = 12.dp).height(50.dp).background(MaterialTheme.colors.background)) { Row(verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.width(4.dp)) - KamelImage(lazyPainterResource(extension, filterQuality = FilterQuality.Medium), extension.name, Modifier.size(50.dp)) + ImageLoaderImage( + data = extension, + contentDescription = extension.name, + modifier = Modifier.size(50.dp), + filterQuality = FilterQuality.Medium + ) Spacer(Modifier.width(8.dp)) Column { val title = buildAnnotatedString { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt index 4c14aea9..f2639ebe 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.unit.dp import ca.gosyer.jui.domain.manga.model.Manga import ca.gosyer.jui.uicore.components.MangaListItem @@ -31,7 +30,6 @@ import ca.gosyer.jui.uicore.components.MangaListItemTitle import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import io.kamel.image.lazyPainterResource @Composable fun LibraryMangaList( @@ -81,7 +79,6 @@ private fun LibraryMangaListItem( showLanguage: Boolean, showLocal: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) MangaListItem( modifier = modifier then Modifier .requiredHeight(56.dp) @@ -91,7 +88,7 @@ private fun LibraryMangaListItem( modifier = Modifier .size(40.dp) .clip(MaterialTheme.shapes.medium), - cover = cover, + data = manga, contentDescription = manga.title ) MangaListItemTitle( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt index c9e9fde4..f10f75ac 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt @@ -35,8 +35,7 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.image.lazyPainterResource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun LibraryMangaComfortableGrid( @@ -94,7 +93,6 @@ private fun LibraryMangaComfortableGridItem( showLanguage: Boolean, showLocal: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp) ) @@ -106,14 +104,15 @@ private fun LibraryMangaComfortableGridItem( .clip(MaterialTheme.shapes.medium) then modifier ) { Column { - KamelImage( - cover, + ImageLoaderImage( + manga, contentDescription = manga.title, modifier = Modifier .fillMaxWidth() .aspectRatio(mangaAspectRatio) .clip(MaterialTheme.shapes.medium), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) Text( text = manga.title, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt index 345ba82a..6bb06af2 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt @@ -38,8 +38,7 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.image.lazyPainterResource +import ca.gosyer.jui.uicore.image.ImageLoaderImage expect fun Modifier.libraryMangaModifier( onClickManga: () -> Unit, @@ -102,7 +101,6 @@ private fun LibraryMangaCompactGridItem( showLanguage: Boolean, showLocal: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp) ) @@ -113,11 +111,12 @@ private fun LibraryMangaCompactGridItem( .aspectRatio(mangaAspectRatio) .clip(MaterialTheme.shapes.medium) then modifier ) { - KamelImage( - cover, + ImageLoaderImage( + manga, manga.title, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) Box(modifier = Modifier.fillMaxSize() then shadowGradient) Text( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt index ba631ec5..e72d8589 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt @@ -29,8 +29,7 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.image.lazyPainterResource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun LibraryMangaCoverOnlyGrid( @@ -88,19 +87,18 @@ private fun LibraryMangaCoverOnlyGridItem( showLanguage: Boolean, showLocal: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) - Box( modifier = Modifier.padding(4.dp) .fillMaxWidth() .aspectRatio(mangaAspectRatio) .clip(MaterialTheme.shapes.medium) then modifier ) { - KamelImage( - cover, + ImageLoaderImage( + manga, contentDescription = manga.title, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) LibraryMangaBadges( modifier = Modifier.padding(4.dp), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt index 92d86148..19f5ad08 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt @@ -41,14 +41,13 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage +import ca.gosyer.jui.uicore.image.ImageLoaderImage import ca.gosyer.jui.uicore.resources.stringResource import com.google.accompanist.flowlayout.FlowRow import com.vanpra.composematerialdialogs.MaterialDialog import com.vanpra.composematerialdialogs.MaterialDialogState import com.vanpra.composematerialdialogs.listItemsMultiChoice import com.vanpra.composematerialdialogs.title -import io.kamel.image.lazyPainterResource @Composable fun MangaItem(manga: Manga) { @@ -74,15 +73,16 @@ fun MangaItem(manga: Manga) { @Composable private fun Cover(manga: Manga, modifier: Modifier = Modifier) { - KamelImage( - resource = lazyPainterResource(manga, filterQuality = FilterQuality.Medium), + ImageLoaderImage( + data = manga, contentDescription = manga.title, modifier = modifier, errorModifier = modifier then Modifier .aspectRatio( ratio = mangaAspectRatio, matchHeightConstraintsFirst = true - ) + ), + filterQuality = FilterQuality.Medium ) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaComfortableGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaComfortableGrid.kt index bf66867f..4c9613ce 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaComfortableGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaComfortableGrid.kt @@ -37,8 +37,7 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.image.lazyPainterResource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun SourceMangaComfortableGrid( @@ -89,7 +88,6 @@ private fun SourceMangaComfortableGridItem( manga: Manga, inLibrary: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp) ) @@ -101,14 +99,15 @@ private fun SourceMangaComfortableGridItem( .clip(MaterialTheme.shapes.medium) then modifier ) { Column { - KamelImage( - cover, + ImageLoaderImage( + manga, contentDescription = manga.title, modifier = Modifier .fillMaxWidth() .aspectRatio(mangaAspectRatio) .clip(MaterialTheme.shapes.medium), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) Text( text = manga.title, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaCompactGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaCompactGrid.kt index 0109c7b6..22bcd309 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaCompactGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaCompactGrid.kt @@ -40,8 +40,7 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.image.lazyPainterResource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun SourceMangaCompactGrid( @@ -92,7 +91,6 @@ private fun SourceMangaCompactGridItem( manga: Manga, inLibrary: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp) ) @@ -103,11 +101,12 @@ private fun SourceMangaCompactGridItem( .aspectRatio(mangaAspectRatio) .clip(MaterialTheme.shapes.medium) then modifier ) { - KamelImage( - cover, + ImageLoaderImage( + manga, manga.title, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) Box(modifier = Modifier.fillMaxSize().then(shadowGradient)) Text( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaList.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaList.kt index 03f61568..86ce4a5c 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaList.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceMangaList.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.unit.dp import ca.gosyer.jui.domain.manga.model.Manga import ca.gosyer.jui.uicore.components.MangaListItem @@ -31,7 +30,6 @@ import ca.gosyer.jui.uicore.components.MangaListItemTitle import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import io.kamel.image.lazyPainterResource @Composable fun SourceMangaList( @@ -74,7 +72,6 @@ private fun MangaListItem( manga: Manga, inLibrary: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) MangaListItem( modifier = modifier then Modifier .requiredHeight(56.dp) @@ -84,7 +81,7 @@ private fun MangaListItem( modifier = Modifier .size(40.dp) .clip(MaterialTheme.shapes.medium), - cover = cover, + data = manga, contentDescription = manga.title ) MangaListItemTitle( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesMenu.kt index f119d8a6..13556dd4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesMenu.kt @@ -40,9 +40,8 @@ import ca.gosyer.jui.ui.sources.home.SourceHomeScreen import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage +import ca.gosyer.jui.uicore.image.ImageLoaderImage import ca.gosyer.jui.uicore.resources.stringResource -import io.kamel.image.lazyPainterResource expect fun Modifier.sourceSideMenuItem( onSourceTabClick: () -> Unit, @@ -140,10 +139,11 @@ fun SourcesSideMenu( modifier = modifier ) is SourceNavigatorScreen.SourceScreen -> Box(Modifier.align(Alignment.Center)) { - KamelImage( - resource = lazyPainterResource(screen.source, filterQuality = FilterQuality.Medium), + ImageLoaderImage( + data = screen.source, contentDescription = screen.source.displayName, - modifier = modifier + modifier = modifier, + filterQuality = FilterQuality.Medium ) } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaComfortableGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaComfortableGrid.kt index c07635ec..981270e4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaComfortableGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaComfortableGrid.kt @@ -28,8 +28,7 @@ import androidx.compose.ui.unit.times import ca.gosyer.jui.domain.manga.model.Manga import ca.gosyer.jui.ui.sources.browse.components.SourceMangaBadges import ca.gosyer.jui.uicore.components.mangaAspectRatio -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.image.lazyPainterResource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun GlobalSearchMangaComfortableGridItem( @@ -37,7 +36,6 @@ fun GlobalSearchMangaComfortableGridItem( manga: Manga, inLibrary: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp) ) @@ -49,14 +47,15 @@ fun GlobalSearchMangaComfortableGridItem( .clip(MaterialTheme.shapes.medium) then modifier ) { Column { - KamelImage( - cover, + ImageLoaderImage( + manga, contentDescription = manga.title, modifier = Modifier .height(200.dp) .aspectRatio(mangaAspectRatio, true) .clip(MaterialTheme.shapes.medium), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) Text( text = manga.title, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaCompactGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaCompactGrid.kt index 40d5122e..ef8adff1 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaCompactGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/components/GlobalSearchMangaCompactGrid.kt @@ -31,8 +31,7 @@ import androidx.compose.ui.unit.sp import ca.gosyer.jui.domain.manga.model.Manga import ca.gosyer.jui.ui.sources.browse.components.SourceMangaBadges import ca.gosyer.jui.uicore.components.mangaAspectRatio -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.image.lazyPainterResource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun GlobalSearchMangaCompactGridItem( @@ -40,7 +39,6 @@ fun GlobalSearchMangaCompactGridItem( manga: Manga, inLibrary: Boolean ) { - val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp) ) @@ -51,11 +49,12 @@ fun GlobalSearchMangaCompactGridItem( .aspectRatio(mangaAspectRatio, true) .clip(MaterialTheme.shapes.medium) then modifier ) { - KamelImage( - cover, + ImageLoaderImage( + manga, manga.title, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) Box(modifier = Modifier.fillMaxSize().then(shadowGradient)) Text( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt index 7787bcad..0643aae6 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt @@ -56,10 +56,9 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.image.KamelImage +import ca.gosyer.jui.uicore.image.ImageLoaderImage import ca.gosyer.jui.uicore.resources.stringResource import com.vanpra.composematerialdialogs.rememberMaterialDialogState -import io.kamel.image.lazyPainterResource @Composable fun SourceHomeScreenContent( @@ -175,7 +174,12 @@ fun WideSourceItem( }, horizontalAlignment = Alignment.CenterHorizontally ) { - KamelImage(lazyPainterResource(source, filterQuality = FilterQuality.Medium), source.displayName, Modifier.size(96.dp)) + ImageLoaderImage( + data = source, + contentDescription = source.displayName, + modifier = Modifier.size(96.dp), + filterQuality = FilterQuality.Medium + ) Spacer(Modifier.height(4.dp)) Text( "${source.name} (${source.displayLang.toUpperCase(Locale.current)})", @@ -199,11 +203,12 @@ fun ThinSourceItem( .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { - KamelImage( - lazyPainterResource(source, filterQuality = FilterQuality.Medium), + ImageLoaderImage( + source, source.displayName, Modifier.fillMaxHeight() - .aspectRatio(1F, true) + .aspectRatio(1F, true), + filterQuality = FilterQuality.Medium ) Spacer(Modifier.width(8.dp)) Column { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt index a4e00c36..910698f3 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import ca.gosyer.jui.domain.chapter.model.Chapter @@ -45,7 +44,6 @@ import ca.gosyer.jui.uicore.components.mangaAspectRatio import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding import ca.gosyer.jui.uicore.resources.stringResource -import io.kamel.image.lazyPainterResource import kotlinx.datetime.LocalDate @Composable @@ -137,7 +135,7 @@ fun UpdatesItem( .padding(start = 16.dp, top = 8.dp, bottom = 8.dp) .clip(MaterialTheme.shapes.medium) .clickable { onClickCover() }, - cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium), + data = manga, contentDescription = manga.title ) MangaListItemColumn( 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 new file mode 100644 index 00000000..8e2717d0 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt @@ -0,0 +1,31 @@ +/* + * 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.core.io.userDataDir +import ca.gosyer.jui.uicore.vm.ContextWrapper +import com.seiko.imageloader.ImageLoaderBuilder +import com.seiko.imageloader.cache.disk.DiskCache +import com.seiko.imageloader.cache.disk.DiskCacheBuilder +import com.seiko.imageloader.cache.memory.MemoryCache +import com.seiko.imageloader.cache.memory.MemoryCacheBuilder + +actual fun imageLoaderBuilder(contextWrapper: ContextWrapper): ImageLoaderBuilder { + return ImageLoaderBuilder() +} + +actual fun diskCache(contextWrapper: ContextWrapper): DiskCache { + return DiskCacheBuilder() + .directory(userDataDir / "image_cache") + .maxSizeBytes(1024 * 1024 * 150) // 150 MB + .build() +} + +actual fun memoryCache(contextWrapper: ContextWrapper): MemoryCache { + return MemoryCacheBuilder() + .build() +} diff --git a/ui-core/build.gradle.kts b/ui-core/build.gradle.kts index dc4d99b9..a2ecf6df 100644 --- a/ui-core/build.gradle.kts +++ b/ui-core/build.gradle.kts @@ -39,6 +39,7 @@ kotlin { api(kotlin("stdlib-common")) api(libs.coroutines.core) api(libs.kamel) + api(libs.imageloader) api(libs.voyager.core) api(libs.dateTime) api(projects.core) diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Manga.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Manga.kt index 36130cdc..2ebabbff 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Manga.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Manga.kt @@ -23,20 +23,19 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.core.Resource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun MangaGridItem( title: String, - cover: Resource, + data: Any, onClick: () -> Unit = {} ) { val fontStyle = LocalTextStyle.current.merge( @@ -53,7 +52,7 @@ fun MangaGridItem( shape = RoundedCornerShape(4.dp) ) { Box(modifier = Modifier.fillMaxSize()) { - KamelImage(cover, title, contentScale = ContentScale.Crop) + ImageLoaderImage(data, title, contentScale = ContentScale.Crop, filterQuality = FilterQuality.Medium) Box(modifier = Modifier.fillMaxSize().then(shadowGradient)) Text( text = title, diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/MangaListItem.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/MangaListItem.kt index 17b3755c..1d522e55 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/MangaListItem.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/MangaListItem.kt @@ -15,12 +15,11 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import ca.gosyer.jui.uicore.image.KamelImage -import io.kamel.core.Resource +import ca.gosyer.jui.uicore.image.ImageLoaderImage @Composable fun MangaListItem( @@ -38,14 +37,15 @@ fun MangaListItem( @Composable fun MangaListItemImage( modifier: Modifier = Modifier, - cover: Resource, + data: Any, contentDescription: String ) { - KamelImage( - cover, + ImageLoaderImage( + data, contentDescription = contentDescription, modifier = modifier, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + filterQuality = FilterQuality.Medium ) } diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/image/ImageLoaderImage.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/image/ImageLoaderImage.kt new file mode 100644 index 00000000..6970eb3e --- /dev/null +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/image/ImageLoaderImage.kt @@ -0,0 +1,143 @@ +/* + * 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.uicore.image + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.BrokenImage +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import ca.gosyer.jui.uicore.components.LoadingScreen +import com.seiko.imageloader.ImageRequestState +import com.seiko.imageloader.rememberAsyncImagePainter +import com.seiko.imageloader.request.ImageRequestBuilder +import org.lighthousegames.logging.logging + +private val log = logging() + +private enum class ImageLoaderImageState { + Loading, + Success, + Failure, +} + +@Composable +fun ImageLoaderImage( + data: Any, + contentDescription: String?, + modifier: Modifier = Modifier, + errorModifier: Modifier = modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DefaultFilterQuality, + onLoading: (@Composable BoxScope.(Float) -> Unit)? = { + LoadingScreen(progress = it.coerceIn(0.0F, 1.0F), modifier = modifier then Modifier.fillMaxSize()) + }, + onFailure: (@Composable BoxScope.(Throwable) -> Unit)? = { + LaunchedEffect(it) { + log.warn(it) { "Error loading image" } + } + Box( + modifier = errorModifier then Modifier.fillMaxSize() + .background(Color(0x1F888888)), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Rounded.BrokenImage, + contentDescription = null, + tint = Color(0x1F888888), + modifier = Modifier.size(24.dp) + ) + } + }, + contentAlignment: Alignment = Alignment.Center, + animationSpec: FiniteAnimationSpec? = tween() +) { + val request = remember(data) { ImageRequestBuilder().data(data).build() } + val painter = rememberAsyncImagePainter(request, contentScale = contentScale) + + val progress = remember { mutableStateOf(-1F) } + val error = remember { mutableStateOf(null) } + val state by derivedStateOf { + when (val state = painter.requestState) { + is ImageRequestState.Failure -> { + progress.value = 0.0F + error.value = state.error + ImageLoaderImageState.Failure + } + ImageRequestState.Loading -> { + progress.value = 0.0F + ImageLoaderImageState.Loading + } + ImageRequestState.Success -> { + progress.value = 1.0F + ImageLoaderImageState.Success + } + } + } + if (animationSpec != null) { + Crossfade(state, animationSpec = animationSpec, modifier = modifier) { + Box(Modifier.fillMaxSize(), contentAlignment) { + when (it) { + ImageLoaderImageState.Loading -> if (onLoading != null) { + onLoading(progress.value) + } + ImageLoaderImageState.Success -> Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.fillMaxSize(), + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter + ) + ImageLoaderImageState.Failure -> { + if (onFailure != null) { + onFailure(error.value ?: return@Crossfade) + } + } + } + } + } + } else { + Box(modifier, contentAlignment) { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.fillMaxSize(), + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter + ) + } + } +} \ No newline at end of file