refactor: migrate image loading to Coil3 (#82)

* update dependency declarations

* update build scripts

* add getTempDir to FileSystemManager

* define ImageLoaderProvider

* refactor ImagePreloadManager

* refactor CustomImage

* update DI and utilities

* update usages in UI components

* update imports for ImagePreloadManager

* cleanup main application class

* add new image transformer for markdown rendering
This commit is contained in:
akesi seli 2024-11-05 16:19:29 +01:00 committed by GitHub
parent 2996c3ea96
commit 325a0dbd03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 344 additions and 347 deletions

View File

@ -1,20 +1,16 @@
package com.livefast.eattrash.raccoonforlemmy.android
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.livefast.eattrash.raccoonforlemmy.core.utils.debug.CrashReportConfiguration
import com.livefast.eattrash.raccoonforlemmy.core.utils.debug.CrashReportWriter
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.getCoilImageLoader
import com.livefast.eattrash.raccoonforlemmy.di.sharedHelperModule
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
class MainApplication :
Application(),
ImageLoaderFactory {
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
@ -40,6 +36,4 @@ class MainApplication :
}
}
}
override fun newImageLoader(): ImageLoader = getCoilImageLoader(this)
}

View File

@ -30,7 +30,6 @@ kotlin {
sourceSets {
val androidMain by getting {
dependencies {
implementation(libs.coil.compose)
implementation(libs.exoplayer)
implementation(libs.exoplayer.dash)
implementation(libs.exoplayer.ui)
@ -44,9 +43,11 @@ kotlin {
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(projects.core.utils)
implementation(libs.coil.compose)
implementation(projects.core.appearance)
implementation(projects.core.l10n)
implementation(projects.core.utils)
}
}
val commonTest by getting {
@ -54,18 +55,19 @@ kotlin {
implementation(kotlin("test"))
}
}
val iosMain by getting {
dependencies {
implementation(libs.kamel)
}
}
}
}
android {
namespace = "com.livefast.eattrash.raccoonforlemmy.core.commonui.components"
compileSdk = libs.versions.android.targetSdk.get().toInt()
compileSdk =
libs.versions.android.targetSdk
.get()
.toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
}

View File

@ -1,104 +0,0 @@
package com.livefast.eattrash.raccoonforlemmy.core.commonui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import com.livefast.eattrash.raccoonforlemmy.core.l10n.messages.LocalStrings
@Composable
actual fun CustomImage(
modifier: Modifier,
url: String,
autoload: Boolean,
blurred: Boolean,
loadButtonContent: @Composable (() -> Unit)?,
contentDescription: String?,
quality: FilterQuality,
contentScale: ContentScale,
alignment: Alignment,
contentAlignment: Alignment,
alpha: Float,
colorFilter: ColorFilter?,
onLoading: @Composable (BoxScope.(Float?) -> Unit)?,
onFailure: @Composable (BoxScope.(Throwable) -> Unit)?,
) {
var shouldBeRendered by remember(autoload) { mutableStateOf(autoload) }
var painterState: AsyncImagePainter.State by remember {
mutableStateOf(AsyncImagePainter.State.Empty)
}
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
if (shouldBeRendered) {
AsyncImage(
modifier =
Modifier
.fillMaxSize()
.blur(radius = if (blurred) 60.dp else 0.dp),
model = url,
contentDescription = contentDescription,
filterQuality = quality,
contentScale = contentScale,
alignment = alignment,
alpha = alpha,
colorFilter = colorFilter,
onLoading = {
painterState = it
},
onError = {
painterState = it
},
onSuccess = {
painterState = it
},
)
when (val state = painterState) {
AsyncImagePainter.State.Empty -> Unit
is AsyncImagePainter.State.Error -> {
onFailure?.invoke(this, state.result.throwable)
}
is AsyncImagePainter.State.Loading -> {
onLoading?.invoke(this, null)
}
else -> Unit
}
} else {
Button(
onClick = {
shouldBeRendered = true
},
) {
if (loadButtonContent != null) {
loadButtonContent.invoke()
} else {
Text(
text = LocalStrings.current.buttonLoad,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}

View File

@ -1,20 +1,35 @@
package com.livefast.eattrash.raccoonforlemmy.core.commonui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import com.livefast.eattrash.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.getImageLoaderProvider
@Composable
expect fun CustomImage(
fun CustomImage(
modifier: Modifier = Modifier,
url: String,
autoload: Boolean = true,
blurred: Boolean = false,
autoload: Boolean,
loadButtonContent: @Composable (() -> Unit)? = null,
contentDescription: String? = null,
quality: FilterQuality = FilterQuality.Medium,
@ -25,4 +40,74 @@ expect fun CustomImage(
colorFilter: ColorFilter? = null,
onLoading: @Composable (BoxScope.(Float?) -> Unit)? = null,
onFailure: @Composable (BoxScope.(Throwable) -> Unit)? = null,
)
onSuccess: @Composable (BoxScope.() -> Unit)? = null,
) {
val imageLoaderProvider = remember { getImageLoaderProvider() }
var painterState: AsyncImagePainter.State by remember {
mutableStateOf(AsyncImagePainter.State.Empty)
}
var shouldBeRendered by remember(autoload) { mutableStateOf(autoload) }
Box(
modifier = modifier,
contentAlignment = contentAlignment,
) {
if (shouldBeRendered) {
AsyncImage(
modifier =
Modifier
.fillMaxSize()
.blur(radius = if (blurred) 60.dp else 0.dp),
model = url,
contentDescription = contentDescription,
filterQuality = quality,
contentScale = contentScale,
alignment = alignment,
alpha = alpha,
colorFilter = colorFilter,
onState = {
painterState = it
},
imageLoader = imageLoaderProvider.provideImageLoader(),
)
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Button(
onClick = {
shouldBeRendered = true
},
) {
if (loadButtonContent != null) {
loadButtonContent.invoke()
} else {
Text(
text = LocalStrings.current.buttonLoad,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
when (val state = painterState) {
AsyncImagePainter.State.Empty -> Unit
is AsyncImagePainter.State.Error -> {
onFailure?.invoke(this, state.result.throwable)
}
is AsyncImagePainter.State.Loading -> {
onLoading?.invoke(this, null)
}
is AsyncImagePainter.State.Success -> {
onSuccess?.invoke(this)
}
else -> Unit
}
}
}

View File

@ -1,86 +0,0 @@
package com.livefast.eattrash.raccoonforlemmy.core.commonui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.livefast.eattrash.raccoonforlemmy.core.l10n.messages.LocalStrings
import io.kamel.image.KamelImage
import io.kamel.image.asyncPainterResource
@Composable
actual fun CustomImage(
modifier: Modifier,
url: String,
autoload: Boolean,
blurred: Boolean,
loadButtonContent: @Composable (() -> Unit)?,
contentDescription: String?,
quality: FilterQuality,
contentScale: ContentScale,
alignment: Alignment,
contentAlignment: Alignment,
alpha: Float,
colorFilter: ColorFilter?,
onLoading: @Composable (BoxScope.(Float?) -> Unit)?,
onFailure: @Composable (BoxScope.(Throwable) -> Unit)?,
) {
var shouldBeRendered by remember(autoload) { mutableStateOf(autoload) }
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
if (shouldBeRendered) {
val painterResource =
asyncPainterResource(
data = url,
filterQuality = quality,
)
KamelImage(
modifier =
Modifier
.fillMaxSize()
.blur(radius = if (blurred) 60.dp else 0.dp),
resource = painterResource,
contentDescription = contentDescription,
contentScale = contentScale,
alignment = alignment,
contentAlignment = contentAlignment,
alpha = alpha,
colorFilter = colorFilter,
onLoading = onLoading,
onFailure = onFailure,
)
} else {
Button(
onClick = {
shouldBeRendered = true
},
) {
if (loadButtonContent != null) {
loadButtonContent()
} else {
Text(
text = LocalStrings.current.buttonLoad,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}

View File

@ -101,6 +101,7 @@ fun CommunityAndCreatorInfo(
onDoubleClick = onDoubleClick ?: {},
),
url = communityIcon,
autoload = autoLoadImages,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
)
@ -133,6 +134,7 @@ fun CommunityAndCreatorInfo(
),
url = creatorAvatar,
quality = FilterQuality.Low,
autoload = autoLoadImages,
contentScale = ContentScale.FillBounds,
)
} else {

View File

@ -64,6 +64,7 @@ fun CommunityHeader(
CustomImage(
modifier = Modifier.fillMaxSize(),
url = banner,
autoload = autoLoadImages,
contentScale = ContentScale.Crop,
contentDescription = null,
)
@ -107,6 +108,7 @@ fun CommunityHeader(
},
),
url = communityIcon,
autoload = autoLoadImages,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
)
@ -181,7 +183,7 @@ fun CommunityHeader(
IconButton(
modifier = Modifier.padding(end = Spacing.s).size(iconSize),
onClick = {
onInfo?.invoke()
onInfo.invoke()
},
) {
Icon(

View File

@ -41,7 +41,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.CustomDrop
import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.CustomImage
import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.PlaceholderImage
import com.livefast.eattrash.raccoonforlemmy.core.utils.compose.buildAnnotatedStringWithHighlights
import com.livefast.eattrash.raccoonforlemmy.core.utils.compose.onClick
import com.livefast.eattrash.raccoonforlemmy.core.utils.toLocalDp
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.readableHandle
@ -93,6 +92,7 @@ fun CommunityItem(
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = communityIcon,
autoload = autoLoadImages,
contentScale = ContentScale.FillBounds,
)
} else {

View File

@ -128,6 +128,7 @@ fun InboxReplySubtitle(
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = creatorAvatar,
autoload = autoLoadImages,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
)
@ -166,6 +167,7 @@ fun InboxReplySubtitle(
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = communityIcon,
autoload = autoLoadImages,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
)

View File

@ -34,7 +34,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.CustomDrop
import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.CustomImage
import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.PlaceholderImage
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.livefast.eattrash.raccoonforlemmy.core.utils.compose.onClick
import com.livefast.eattrash.raccoonforlemmy.core.utils.toLocalDp
@Composable
@ -67,6 +66,7 @@ fun MultiCommunityItem(
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = communityIcon,
autoload = autoLoadImages,
contentScale = ContentScale.FillBounds,
)
} else {

View File

@ -60,6 +60,7 @@ fun SettingsImageInfo(
CustomImage(
modifier = imageModifier,
url = url,
autoload = true,
quality = FilterQuality.Low,
contentScale = contentScale,
)

View File

@ -67,6 +67,7 @@ fun UserHeader(
CustomImage(
modifier = Modifier.fillMaxSize(),
url = banner,
autoload = autoLoadImages,
quality = FilterQuality.Low,
contentScale = ContentScale.Crop,
)
@ -107,6 +108,7 @@ fun UserHeader(
},
),
url = userAvatar,
autoload = autoLoadImages,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
)
@ -209,7 +211,7 @@ fun UserHeader(
IconButton(
modifier = Modifier.padding(end = Spacing.s).size(iconSize),
onClick = {
onInfo?.invoke()
onInfo.invoke()
},
) {
Icon(

View File

@ -38,7 +38,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.CustomDrop
import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.CustomImage
import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.PlaceholderImage
import com.livefast.eattrash.raccoonforlemmy.core.utils.compose.buildAnnotatedStringWithHighlights
import com.livefast.eattrash.raccoonforlemmy.core.utils.compose.onClick
import com.livefast.eattrash.raccoonforlemmy.core.utils.toLocalDp
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.UserModel
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.readableHandle
@ -81,6 +80,7 @@ fun UserItem(
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = avatar,
autoload = autoLoadImages,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
)

View File

@ -30,7 +30,7 @@ kotlin {
sourceSets {
val androidMain by getting {
dependencies {
implementation(libs.multiplatform.markdown.renderer.coil2)
implementation(libs.multiplatform.markdown.renderer.coil3)
}
}
val commonMain by getting {
@ -41,6 +41,7 @@ kotlin {
api(libs.multiplatform.markdown.renderer)
api(libs.multiplatform.markdown.renderer.m3)
api(libs.multiplatform.markdown.renderer.coil3)
implementation(projects.core.l10n)
implementation(projects.core.commonui.components)
@ -57,8 +58,14 @@ kotlin {
android {
namespace = "com.livefast.eattrash.raccoonforlemmy.core.markdown"
compileSdk = libs.versions.android.targetSdk.get().toInt()
compileSdk =
libs.versions.android.targetSdk
.get()
.toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
}

View File

@ -1,6 +0,0 @@
package com.livefast.eattrash.raccoonforlemmy.core.markdown
import com.mikepenz.markdown.coil2.Coil2ImageTransformerImpl
import com.mikepenz.markdown.model.ImageTransformer
actual fun provideImageTransformer(): ImageTransformer = Coil2ImageTransformerImpl

View File

@ -1,5 +1,6 @@
package com.livefast.eattrash.raccoonforlemmy.core.markdown
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
import com.mikepenz.markdown.model.ImageTransformer
expect fun provideImageTransformer(): ImageTransformer
internal fun provideImageTransformer(): ImageTransformer = Coil3ImageTransformerImpl

View File

@ -1,6 +0,0 @@
package com.livefast.eattrash.raccoonforlemmy.core.markdown
import com.mikepenz.markdown.model.ImageTransformer
import com.mikepenz.markdown.model.NoOpImageTransformerImpl
actual fun provideImageTransformer(): ImageTransformer = NoOpImageTransformerImpl()

View File

@ -36,6 +36,8 @@ kotlin {
implementation(libs.koin.core)
implementation(libs.ktor.cio)
implementation(libs.coil)
implementation(libs.coil.network.ktor)
implementation(project.dependencies.platform(libs.kotlincrypto.bom))
implementation(libs.kotlincrypto.md5)
@ -48,7 +50,6 @@ kotlin {
implementation(libs.androidx.activity)
implementation(libs.androidx.browser)
implementation(libs.ktor.android)
implementation(libs.coil)
implementation(libs.coil.gif)
}
}
@ -71,8 +72,14 @@ kotlin {
android {
namespace = "com.livefast.eattrash.raccoonforlemmy.core.utils"
compileSdk = libs.versions.android.targetSdk.get().toInt()
compileSdk =
libs.versions.android.targetSdk
.get()
.toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
}

View File

@ -12,8 +12,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.utils.fs.DefaultFileSystemMana
import com.livefast.eattrash.raccoonforlemmy.core.utils.fs.FileSystemManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.gallery.DefaultGalleryHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.gallery.GalleryHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.DefaultImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.network.DefaultNetworkManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.network.NetworkManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.share.DefaultShareHelper
@ -24,15 +22,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.DefaultHapticFee
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import org.koin.dsl.module
actual val imagePreloadModule =
module {
single<ImagePreloadManager> {
DefaultImagePreloadManager(
context = get(),
)
}
}
actual val networkModule =
module {
single<NetworkManager> {

View File

@ -5,6 +5,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import okio.FileSystem
import okio.Path
import org.koin.java.KoinJavaComponent
import java.io.InputStreamReader
import java.io.OutputStreamWriter
@ -55,6 +57,8 @@ class DefaultFileSystemManager(
launcher.launch(name)
}
}
override fun getTempDir(): Path = FileSystem.SYSTEM_TEMPORARY_DIRECTORY
}
actual fun getFileSystemManager(): FileSystemManager {

View File

@ -0,0 +1,33 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imageload
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import coil3.decode.Decoder
import coil3.gif.AnimatedImageDecoder
import coil3.gif.GifDecoder
import org.koin.java.KoinJavaComponent
actual fun ByteArray.toComposeImageBitmap(): ImageBitmap = BitmapFactory.decodeByteArray(this, 0, size).asImageBitmap()
actual fun IntArray.toComposeImageBitmap(
width: Int,
height: Int,
): ImageBitmap = Bitmap.createBitmap(this, width, height, Bitmap.Config.ARGB_8888).asImageBitmap()
actual fun getNativeDecoders(): List<Decoder.Factory> =
buildList {
if (Build.VERSION.SDK_INT >= 28) {
AnimatedImageDecoder.Factory()
} else {
GifDecoder.Factory()
}
}
actual fun getImageLoaderProvider(): ImageLoaderProvider {
val inject = KoinJavaComponent.inject<ImageLoaderProvider>(ImageLoaderProvider::class.java)
val res by inject
return res
}

View File

@ -1,25 +0,0 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload
import android.content.Context
import coil.annotation.ExperimentalCoilApi
import coil.imageLoader
import coil.memory.MemoryCache
import coil.request.ImageRequest
class DefaultImagePreloadManager(
private val context: Context,
) : ImagePreloadManager {
override fun preload(url: String) {
val request =
ImageRequest.Builder(context)
.data(url)
.build()
context.imageLoader.enqueue(request)
}
@OptIn(ExperimentalCoilApi::class)
override fun remove(url: String) {
context.imageLoader.memoryCache?.remove(MemoryCache.Key(url))
context.imageLoader.diskCache?.remove(url)
}
}

View File

@ -1,32 +0,0 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload
import android.content.Context
import android.os.Build
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.memory.MemoryCache
fun getCoilImageLoader(context: Context): ImageLoader =
ImageLoader.Builder(context)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.memoryCache {
MemoryCache.Builder(context)
.maxSizePercent(0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizePercent(0.02)
.build()
}
.crossfade(true)
.build()

View File

@ -2,8 +2,6 @@ package com.livefast.eattrash.raccoonforlemmy.core.utils.di
import org.koin.core.module.Module
expect val imagePreloadModule: Module
expect val networkModule: Module
expect val appIconModule: Module

View File

@ -1,5 +1,9 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.di
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.DefaultImageLoaderProvider
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.DefaultImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImageLoaderProvider
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.zombiemode.DefaultZombieModeHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.zombiemode.ZombieModeHelper
import org.koin.dsl.module
@ -9,4 +13,17 @@ val utilsModule =
factory<ZombieModeHelper> {
DefaultZombieModeHelper()
}
single<ImageLoaderProvider> {
DefaultImageLoaderProvider(
context = get(),
fileSystemManager = get(),
)
}
single<ImagePreloadManager> {
DefaultImagePreloadManager(
context = get(),
imageLoaderProvider = get(),
)
}
}

View File

@ -1,6 +1,7 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.fs
import androidx.compose.runtime.Composable
import okio.Path
interface FileSystemManager {
val isSupported: Boolean
@ -18,6 +19,8 @@ interface FileSystemManager {
data: String,
callback: (Boolean) -> Unit,
)
fun getTempDir(): Path
}
expect fun getFileSystemManager(): FileSystemManager

View File

@ -0,0 +1,39 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imageload
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.request.crossfade
import com.livefast.eattrash.raccoonforlemmy.core.utils.fs.FileSystemManager
internal class DefaultImageLoaderProvider(
private val context: PlatformContext,
private val fileSystemManager: FileSystemManager,
) : ImageLoaderProvider {
private val imageLoader by lazy {
ImageLoader
.Builder(context)
.components {
val decoders = getNativeDecoders()
for (decoder in decoders) {
add(decoder)
}
}.memoryCache {
MemoryCache
.Builder()
.maxSizePercent(context, 0.25)
.build()
}.diskCache {
val path = fileSystemManager.getTempDir()
DiskCache
.Builder()
.directory(path)
.maxSizePercent(0.02)
.build()
}.crossfade(true)
.build()
}
override fun provideImageLoader(): ImageLoader = imageLoader
}

View File

@ -0,0 +1,26 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imageload
import coil3.PlatformContext
import coil3.memory.MemoryCache
import coil3.request.ImageRequest
internal class DefaultImagePreloadManager(
private val context: PlatformContext,
private val imageLoaderProvider: ImageLoaderProvider,
) : ImagePreloadManager {
override fun preload(url: String) {
val imageLoader = imageLoaderProvider.provideImageLoader()
val request =
ImageRequest
.Builder(context)
.data(url)
.build()
imageLoader.enqueue(request)
}
override fun remove(url: String) {
val imageLoader = imageLoaderProvider.provideImageLoader()
imageLoader.memoryCache?.remove(MemoryCache.Key(url))
imageLoader.diskCache?.remove(url)
}
}

View File

@ -0,0 +1,7 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imageload
import coil3.ImageLoader
interface ImageLoaderProvider {
fun provideImageLoader(): ImageLoader
}

View File

@ -1,4 +1,4 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload
package com.livefast.eattrash.raccoonforlemmy.core.utils.imageload
interface ImagePreloadManager {
fun preload(url: String)

View File

@ -0,0 +1,15 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imageload
import androidx.compose.ui.graphics.ImageBitmap
import coil3.decode.Decoder
expect fun ByteArray.toComposeImageBitmap(): ImageBitmap
expect fun IntArray.toComposeImageBitmap(
width: Int,
height: Int,
): ImageBitmap
expect fun getNativeDecoders(): List<Decoder.Factory>
expect fun getImageLoaderProvider(): ImageLoaderProvider

View File

@ -12,8 +12,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.utils.fs.DefaultFileSystemMana
import com.livefast.eattrash.raccoonforlemmy.core.utils.fs.FileSystemManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.gallery.DefaultGalleryHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.gallery.GalleryHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.DefaultImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.network.DefaultNetworkManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.network.NetworkManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.share.DefaultShareHelper
@ -24,13 +22,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.DefaultHapticFee
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import org.koin.dsl.module
actual val imagePreloadModule =
module {
single<ImagePreloadManager> {
DefaultImagePreloadManager()
}
}
actual val networkModule =
module {
single<NetworkManager> { DefaultNetworkManager() }

View File

@ -1,6 +1,8 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.fs
import androidx.compose.runtime.Composable
import okio.FileSystem
import okio.Path
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -24,6 +26,8 @@ class DefaultFileSystemManager : FileSystemManager {
) {
callback(false)
}
override fun getTempDir(): Path = FileSystem.SYSTEM_TEMPORARY_DIRECTORY
}
object FileSystemManagerDiHelper : KoinComponent {

View File

@ -0,0 +1,33 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imageload
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asComposeImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import coil3.decode.Decoder
import org.jetbrains.skia.Bitmap
import org.jetbrains.skia.ColorAlphaType
import org.jetbrains.skia.ColorType
import org.jetbrains.skia.Image
import org.jetbrains.skia.ImageInfo
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
actual fun ByteArray.toComposeImageBitmap(): ImageBitmap = Image.makeFromEncoded(this).toComposeImageBitmap()
actual fun IntArray.toComposeImageBitmap(
width: Int,
height: Int,
): ImageBitmap {
val bmp = Bitmap()
val info = ImageInfo(width, height, ColorType.RGBA_8888, ColorAlphaType.PREMUL)
bmp.installPixels(info, map { it.toByte() }.toByteArray(), info.minRowBytes)
return bmp.asComposeImageBitmap()
}
actual fun getNativeDecoders(): List<Decoder.Factory> = emptyList()
actual fun getImageLoaderProvider(): ImageLoaderProvider = ImageUtilsDiHelper.imageLoaderProvider
internal object ImageUtilsDiHelper : KoinComponent {
val imageLoaderProvider: ImageLoaderProvider by inject()
}

View File

@ -1,11 +0,0 @@
package com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload
class DefaultImagePreloadManager() : ImagePreloadManager {
override fun preload(url: String) {
// no-op
}
override fun remove(url: String) {
// no-op
}
}

View File

@ -8,7 +8,7 @@ androidx-media3 = "1.4.1"
androidx-splashscreen = "1.0.1"
androidx-work = "2.9.1"
android-gradle = "8.5.2"
coil = "2.7.0"
coil = "3.0.0"
colorpicker = "1.1.2"
compose = "1.7.0"
detekt = "1.23.7"
@ -47,16 +47,16 @@ androidx-security-crypto = { module = "androidx.security:security-crypto", versi
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
kamel = { module = "media.kamel:kamel-image", version.ref = "kamel" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" }
coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
compose-colorpicker = { module = "com.github.skydoves:colorpicker-compose", version.ref = "colorpicker" }
multiplatform-markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "multiplatform-markdown-renderer" }
multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatform-markdown-renderer" }
multiplatform-markdown-renderer-coil2 = { module = "com.mikepenz:multiplatform-markdown-renderer-coil2", version.ref = "multiplatform-markdown-renderer" }
multiplatform-markdown-renderer-coil3= { module = "com.mikepenz:multiplatform-markdown-renderer-coil3", version.ref = "multiplatform-markdown-renderer" }
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" }

View File

@ -17,7 +17,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.utils.di.customTabsModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.fileSystemModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.galleryHelperModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.hapticFeedbackModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.imagePreloadModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.networkModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.shareHelperModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.utilsModule
@ -79,7 +78,6 @@ val sharedHelperModule =
repositoryModule,
utilsModule,
domainInboxModule,
imagePreloadModule,
networkModule,
coreNavigationModule,
lemmyUiModule,

View File

@ -81,12 +81,13 @@ internal fun RowScope.TabNavigationItem(
val iconSize = IconSize.m
CustomImage(
url = customIconUrl,
modifier =
Modifier
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2))
.then(pointerInputModifier),
url = customIconUrl,
autoload = true,
)
} else {
Icon(

View File

@ -17,7 +17,6 @@ import com.livefast.eattrash.raccoonforlemmy.core.utils.di.customTabsModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.fileSystemModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.galleryHelperModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.hapticFeedbackModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.imagePreloadModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.networkModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.shareHelperModule
import com.livefast.eattrash.raccoonforlemmy.core.utils.di.utilsModule
@ -79,7 +78,6 @@ fun initKoin() {
repositoryModule,
domainInboxModule,
utilsModule,
imagePreloadModule,
networkModule,
coreNavigationModule,
lemmyUiModule,

View File

@ -47,6 +47,7 @@ fun AcknoledgementItem(
.clip(RoundedCornerShape(iconSize / 2)),
contentDescription = null,
url = url,
autoload = true,
)
}
} else {

View File

@ -14,7 +14,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.Communi
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunitySortRepository
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.FavoriteCommunityRepository
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.share.ShareHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.livefast.eattrash.raccoonforlemmy.core.utils.zombiemode.ZombieModeHelper

View File

@ -45,6 +45,7 @@ internal fun DrawerCommunityItem(
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
url = url,
autoload = autoLoadImages,
contentScale = ContentScale.FillBounds,
)
} else {

View File

@ -8,7 +8,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.architecture.DefaultMviModel
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenter
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository
import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.IdentityRepository
@ -390,7 +390,10 @@ class ExploreViewModel(
val results = paginationManager.loadNextPage()
if (uiState.value.autoLoadImages) {
results.forEach { res ->
(res as? SearchResult.Post)?.model?.imageUrl?.takeIf { it.isNotEmpty() }
(res as? SearchResult.Post)
?.model
?.imageUrl
?.takeIf { it.isNotEmpty() }
?.also { url ->
imagePreloadManager.preload(url)
}

View File

@ -11,7 +11,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.architecture.DefaultMviModel
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenter
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.CommentModel

View File

@ -175,6 +175,7 @@ private fun ModlogHeader(
url = creatorAvatar,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
autoload = autoLoadImages,
)
}
Text(
@ -197,7 +198,6 @@ private fun ModlogFooter(
onOpen: (() -> Unit)? = null,
onOptionSelected: ((OptionId) -> Unit)? = null,
) {
val buttonModifier = Modifier.size(IconSize.l).padding(3.dp)
var optionsExpanded by remember { mutableStateOf(false) }
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ancillaryTextAlpha)

View File

@ -11,7 +11,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCent
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.share.ShareHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.IdentityRepository

View File

@ -160,8 +160,8 @@ class MultiCommunityEditorScreen(
value = uiState.name,
keyboardOptions =
KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Text,
autoCorrect = false,
imeAction = ImeAction.Next,
),
onValueChange = { value ->
@ -227,6 +227,7 @@ class MultiCommunityEditorScreen(
},
),
url = url,
autoload = uiState.autoLoadImages,
contentScale = ContentScale.FillBounds,
)
}

View File

@ -9,7 +9,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.architecture.DefaultMviModel
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenter
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.share.ShareHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.livefast.eattrash.raccoonforlemmy.core.utils.zombiemode.ZombieModeHelper

View File

@ -171,6 +171,7 @@ private fun ReportHeader(
url = creatorAvatar,
quality = FilterQuality.Low,
contentScale = ContentScale.FillBounds,
autoload = autoLoadImages,
)
}
Text(
@ -193,7 +194,6 @@ private fun ReportFooter(
onOpenResolve: (() -> Unit)? = null,
onOptionSelected: ((OptionId) -> Unit)? = null,
) {
val buttonModifier = Modifier.size(IconSize.l).padding(3.dp)
var optionsExpanded by remember { mutableStateOf(false) }
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ancillaryTextAlpha)

View File

@ -12,7 +12,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.UserDetailSec
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenter
import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.share.ShareHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository

View File

@ -9,7 +9,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.Setting
import com.livefast.eattrash.raccoonforlemmy.core.utils.datetime.epochMillis
import com.livefast.eattrash.raccoonforlemmy.core.utils.gallery.GalleryHelper
import com.livefast.eattrash.raccoonforlemmy.core.utils.gallery.download
import com.livefast.eattrash.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.imageload.ImagePreloadManager
import com.livefast.eattrash.raccoonforlemmy.core.utils.share.ShareHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers