diff --git a/android_kmp/craftd-compose/build.gradle.kts b/android_kmp/craftd-compose/build.gradle.kts index c71f863..2f17f9e 100644 --- a/android_kmp/craftd-compose/build.gradle.kts +++ b/android_kmp/craftd-compose/build.gradle.kts @@ -6,9 +6,7 @@ plugins { } kotlin { - androidTarget { - publishLibraryVariants("release", "debug") - } + androidTarget { publishLibraryVariants("release", "debug") } sourceSets { androidMain.dependencies { implementation(libs.androidx.core) @@ -24,4 +22,6 @@ kotlin { implementation(libs.kotlinx.collections.immutable) } } -} \ No newline at end of file +} + + diff --git a/android_kmp/craftd-compose/src/androidMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImage.kt b/android_kmp/craftd-compose/src/androidMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImage.kt new file mode 100644 index 0000000..a79142c --- /dev/null +++ b/android_kmp/craftd-compose/src/androidMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImage.kt @@ -0,0 +1,65 @@ +package com.github.codandotv.craftd.compose.ui.image + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import coil3.compose.AsyncImage +import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties +import com.github.codandotv.craftd.compose.extensions.toArrangementCompose + +/** + * CraftDImage composable for rendering images using Coil 3. + * + * Supports both local and network images via Coil's AsyncImage. + */ +@Composable +fun CraftDImage( + imageProperties: ImageProperties, + modifier: Modifier = Modifier, + clickable: (() -> Unit)? = null, +) { + val modifierCustom = clickable?.let { Modifier.clickable { clickable.invoke() } } ?: modifier + + Row( + horizontalArrangement = imageProperties.align.toArrangementCompose(), + modifier = Modifier.fillMaxWidth() + ) { + AsyncImage( + model = imageProperties.url, + contentDescription = imageProperties.contentDescription, + modifier = + modifierCustom + .then( + if (imageProperties.fillMaxSize == true) { + Modifier.fillMaxSize() + } else { + Modifier + } + ) + .then( + imageProperties.aspectRatio?.let { ratio -> + Modifier.aspectRatio(ratio) + } + ?: Modifier + ), + contentScale = imageProperties.contentScale?.toContentScale() ?: ContentScale.Fit + ) + } +} + +private fun String.toContentScale(): ContentScale = + when (this.lowercase()) { + "crop" -> ContentScale.Crop + "fit" -> ContentScale.Fit + "fillbounds" -> ContentScale.FillBounds + "fillwidth" -> ContentScale.FillWidth + "fillheight" -> ContentScale.FillHeight + "inside" -> ContentScale.Inside + "none" -> ContentScale.None + else -> ContentScale.Fit + } diff --git a/android_kmp/craftd-compose/src/androidMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImageBuilder.kt b/android_kmp/craftd-compose/src/androidMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImageBuilder.kt new file mode 100644 index 0000000..4dcd382 --- /dev/null +++ b/android_kmp/craftd-compose/src/androidMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImageBuilder.kt @@ -0,0 +1,20 @@ +package com.github.codandotv.craftd.compose.ui.image + +import androidx.compose.runtime.Composable +import com.github.codandotv.craftd.androidcore.data.convertToElement +import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties +import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties +import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey +import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener +import com.github.codandotv.craftd.compose.builder.CraftDBuilder + +class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key) : + CraftDBuilder { + @Composable + override fun craft(model: SimpleProperties, listener: CraftDViewListener) { + val imageProperties = model.value.convertToElement() + imageProperties?.let { + CraftDImage(it) { imageProperties.actionProperties?.let { listener.invoke(it) } } + } + } +} diff --git a/android_kmp/craftd-compose/src/commonMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImage.kt b/android_kmp/craftd-compose/src/commonMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImage.kt new file mode 100644 index 0000000..a79142c --- /dev/null +++ b/android_kmp/craftd-compose/src/commonMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImage.kt @@ -0,0 +1,65 @@ +package com.github.codandotv.craftd.compose.ui.image + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import coil3.compose.AsyncImage +import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties +import com.github.codandotv.craftd.compose.extensions.toArrangementCompose + +/** + * CraftDImage composable for rendering images using Coil 3. + * + * Supports both local and network images via Coil's AsyncImage. + */ +@Composable +fun CraftDImage( + imageProperties: ImageProperties, + modifier: Modifier = Modifier, + clickable: (() -> Unit)? = null, +) { + val modifierCustom = clickable?.let { Modifier.clickable { clickable.invoke() } } ?: modifier + + Row( + horizontalArrangement = imageProperties.align.toArrangementCompose(), + modifier = Modifier.fillMaxWidth() + ) { + AsyncImage( + model = imageProperties.url, + contentDescription = imageProperties.contentDescription, + modifier = + modifierCustom + .then( + if (imageProperties.fillMaxSize == true) { + Modifier.fillMaxSize() + } else { + Modifier + } + ) + .then( + imageProperties.aspectRatio?.let { ratio -> + Modifier.aspectRatio(ratio) + } + ?: Modifier + ), + contentScale = imageProperties.contentScale?.toContentScale() ?: ContentScale.Fit + ) + } +} + +private fun String.toContentScale(): ContentScale = + when (this.lowercase()) { + "crop" -> ContentScale.Crop + "fit" -> ContentScale.Fit + "fillbounds" -> ContentScale.FillBounds + "fillwidth" -> ContentScale.FillWidth + "fillheight" -> ContentScale.FillHeight + "inside" -> ContentScale.Inside + "none" -> ContentScale.None + else -> ContentScale.Fit + } diff --git a/android_kmp/craftd-compose/src/commonMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImageBuilder.kt b/android_kmp/craftd-compose/src/commonMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImageBuilder.kt new file mode 100644 index 0000000..4dcd382 --- /dev/null +++ b/android_kmp/craftd-compose/src/commonMain/kotlin/com/github/codandotv/craftd/compose/ui/image/CraftDImageBuilder.kt @@ -0,0 +1,20 @@ +package com.github.codandotv.craftd.compose.ui.image + +import androidx.compose.runtime.Composable +import com.github.codandotv.craftd.androidcore.data.convertToElement +import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties +import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties +import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey +import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener +import com.github.codandotv.craftd.compose.builder.CraftDBuilder + +class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key) : + CraftDBuilder { + @Composable + override fun craft(model: SimpleProperties, listener: CraftDViewListener) { + val imageProperties = model.value.convertToElement() + imageProperties?.let { + CraftDImage(it) { imageProperties.actionProperties?.let { listener.invoke(it) } } + } + } +} diff --git a/android_kmp/craftd-core/src/commonMain/kotlin/com/github/codandotv/craftd/androidcore/data/model/image/ImageProperties.kt b/android_kmp/craftd-core/src/commonMain/kotlin/com/github/codandotv/craftd/androidcore/data/model/image/ImageProperties.kt new file mode 100644 index 0000000..3568ee5 --- /dev/null +++ b/android_kmp/craftd-core/src/commonMain/kotlin/com/github/codandotv/craftd/androidcore/data/model/image/ImageProperties.kt @@ -0,0 +1,21 @@ +package com.github.codandotv.craftd.androidcore.data.model.image + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.github.codandotv.craftd.androidcore.data.model.action.ActionProperties +import com.github.codandotv.craftd.androidcore.domain.CraftDAlign +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@Immutable +@Stable +data class ImageProperties( + @SerialName("url") val url: String? = null, + @SerialName("contentDescription") val contentDescription: String? = null, + @SerialName("align") val align: CraftDAlign? = null, + @SerialName("fillMaxSize") val fillMaxSize: Boolean? = false, + @SerialName("aspectRatio") val aspectRatio: Float? = null, + @SerialName("contentScale") val contentScale: String? = null, + @SerialName("actionProperties") var actionProperties: ActionProperties? = null, +) diff --git a/android_kmp/craftd-core/src/commonMain/kotlin/com/github/codandotv/craftd/androidcore/presentation/CraftDComponentKey.kt b/android_kmp/craftd-core/src/commonMain/kotlin/com/github/codandotv/craftd/androidcore/presentation/CraftDComponentKey.kt index 92e7529..a6ffb19 100644 --- a/android_kmp/craftd-core/src/commonMain/kotlin/com/github/codandotv/craftd/androidcore/presentation/CraftDComponentKey.kt +++ b/android_kmp/craftd-core/src/commonMain/kotlin/com/github/codandotv/craftd/androidcore/presentation/CraftDComponentKey.kt @@ -1,10 +1,10 @@ package com.github.codandotv.craftd.androidcore.presentation - enum class CraftDComponentKey(val key: String) { TEXT_VIEW_COMPONENT("${CRAFT_D}TextView"), BUTTON_COMPONENT("${CRAFT_D}Button"), CHECK_BOX_COMPONENT("${CRAFT_D}CheckBox"), + IMAGE_COMPONENT("${CRAFT_D}Image"), } -internal const val CRAFT_D = "CraftD" \ No newline at end of file +internal const val CRAFT_D = "CraftD" diff --git a/android_kmp/craftd-xml/build.gradle.kts b/android_kmp/craftd-xml/build.gradle.kts index 332991a..80e391f 100644 --- a/android_kmp/craftd-xml/build.gradle.kts +++ b/android_kmp/craftd-xml/build.gradle.kts @@ -1,8 +1,4 @@ -android{ - buildFeatures{ - viewBinding = true - } -} +android { buildFeatures { viewBinding = true } } plugins { id("com.codandotv.android-library") @@ -14,4 +10,4 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.appcompat) implementation(libs.google.material) -} \ No newline at end of file +} diff --git a/android_kmp/craftd-xml/src/main/kotlin/com/github/codandotv/craftd/xml/ui/image/CraftDImageComponent.kt b/android_kmp/craftd-xml/src/main/kotlin/com/github/codandotv/craftd/xml/ui/image/CraftDImageComponent.kt new file mode 100644 index 0000000..e7d4a1e --- /dev/null +++ b/android_kmp/craftd-xml/src/main/kotlin/com/github/codandotv/craftd/xml/ui/image/CraftDImageComponent.kt @@ -0,0 +1,97 @@ +package com.github.codandotv.craftd.xml.ui.image + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.RelativeLayout +import coil3.load +import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties +import com.github.codandotv.craftd.androidcore.domain.CraftDAlign +import com.github.codandotv.craftd.xml.databinding.ImageBinding + +/** + * CraftDImageComponent for XML-based image rendering using Coil 3. + * + * Supports both local and network images via Coil's load extension. + */ +class CraftDImageComponent +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : RelativeLayout(context, attrs, defStyleAttr) { + private var binding: ImageBinding + + init { + binding = ImageBinding.inflate(LayoutInflater.from(context), this) + } + + fun setProperties(imageProperties: ImageProperties) { + // Load image using Coil + imageProperties.url?.let { url -> binding.imageView.load(url) } + + imageProperties.contentDescription?.let { description -> + binding.imageView.contentDescription = description + } + + setupFillMaxSize(imageProperties) + + imageProperties.aspectRatio?.let { ratio -> + binding.imageView.adjustViewBounds = true + // AspectRatio can be handled via custom logic or ConstraintLayout + } + + imageProperties.contentScale?.let { scale -> + binding.imageView.scaleType = scale.toScaleType() + } + + imageProperties.align?.let { + binding.imageView.layoutParams = + LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + .apply { addRule(it.toRelativeLayoutParams()) } + } + } + + private fun setupFillMaxSize(imageProperties: ImageProperties) { + binding.imageView.layoutParams = + imageProperties.fillMaxSize?.let { isFillMaxSize -> + LayoutParams( + if (isFillMaxSize) { + ViewGroup.LayoutParams.MATCH_PARENT + } else { + ViewGroup.LayoutParams.WRAP_CONTENT + }, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + ?: LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + private fun CraftDAlign?.toRelativeLayoutParams(): Int = + when (this) { + CraftDAlign.CENTER -> CENTER_IN_PARENT + CraftDAlign.RIGHT -> ALIGN_PARENT_END + else -> ALIGN_PARENT_START + } + + private fun String.toScaleType(): ImageView.ScaleType = + when (this.lowercase()) { + "crop" -> ImageView.ScaleType.CENTER_CROP + "fit" -> ImageView.ScaleType.FIT_CENTER + "fillbounds" -> ImageView.ScaleType.FIT_XY + "fillwidth" -> ImageView.ScaleType.FIT_START + "fillheight" -> ImageView.ScaleType.FIT_END + "inside" -> ImageView.ScaleType.CENTER_INSIDE + "none" -> ImageView.ScaleType.CENTER + else -> ImageView.ScaleType.FIT_CENTER + } +} diff --git a/android_kmp/craftd-xml/src/main/kotlin/com/github/codandotv/craftd/xml/ui/image/CraftDImageComponentRender.kt b/android_kmp/craftd-xml/src/main/kotlin/com/github/codandotv/craftd/xml/ui/image/CraftDImageComponentRender.kt new file mode 100644 index 0000000..7e4b9f7 --- /dev/null +++ b/android_kmp/craftd-xml/src/main/kotlin/com/github/codandotv/craftd/xml/ui/image/CraftDImageComponentRender.kt @@ -0,0 +1,32 @@ +package com.github.codandotv.craftd.xml.ui.image + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.github.codandotv.craftd.androidcore.data.convertToElement +import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties +import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties +import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey +import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener +import com.github.codandotv.craftd.xml.ui.CraftDViewRenderer + +class ImageComponentRender(override var onClickListener: CraftDViewListener?) : + CraftDViewRenderer( + CraftDComponentKey.IMAGE_COMPONENT.key, + CraftDComponentKey.IMAGE_COMPONENT.ordinal + ) { + + inner class ImageHolder(val image: CraftDImageComponent) : RecyclerView.ViewHolder(image) + + override fun bindView(model: SimpleProperties, holder: ImageHolder, position: Int) { + val imageProperties = model.value.convertToElement() + + imageProperties?.let { holder.image.setProperties(it) } + imageProperties?.actionProperties?.let { actionProperties -> + holder.image.setOnClickListener { onClickListener?.invoke(actionProperties) } + } + } + + override fun createViewHolder(parent: ViewGroup): ImageHolder { + return ImageHolder(CraftDImageComponent(parent.context)) + } +} diff --git a/android_kmp/craftd-xml/src/main/res/layout/image.xml b/android_kmp/craftd-xml/src/main/res/layout/image.xml new file mode 100644 index 0000000..ab41f5e --- /dev/null +++ b/android_kmp/craftd-xml/src/main/res/layout/image.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/android_kmp/gradle/libs.versions.toml b/android_kmp/gradle/libs.versions.toml index 9207595..e5f4dbb 100644 --- a/android_kmp/gradle/libs.versions.toml +++ b/android_kmp/gradle/libs.versions.toml @@ -20,6 +20,9 @@ kotlinx-collections-immutable = "0.4.0" kotlinx-serialization = "1.9.0" kotlinx-coroutines = "1.10.2" +# Coil +coil = "3.3.0" + # Jackson jackson = "2.17.2" @@ -59,6 +62,10 @@ compose_lifecycle = { group = "androidx.lifecycle", name = "lifecycle-runtime-co # KotlinX kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" } +# Coil +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } + # Jackson fasterxml_jackson = { group = "com.fasterxml.jackson.core", name = "jackson-core", version.ref = "jackson" } fasterxml_jackson_databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" }